├── keywords.txt ├── library.properties ├── LICENSE ├── Norman.h ├── examples ├── Example_Test │ └── Example_Test.ino └── Example_Practical │ └── Example_Practical.ino ├── parameters.txt ├── README.md └── Norman.cpp /keywords.txt: -------------------------------------------------------------------------------- 1 | SetTarget_Origin KEYWORD1 2 | SetRangeTDP KEYWORD1 3 | getTime KEYWORD2 4 | DSTCheck KEYWORD2 5 | RHtoDP KEYWORD2 6 | getSun KEYWORD2 7 | getTDP KEYWORD2 8 | getRH KEYWORD2 9 | getYear KEYWORD2 10 | getMonth KEYWORD2 11 | getDay KEYWORD2 12 | getHour KEYWORD2 13 | getMinute KEYWORD2 14 | getSecond KEYWORD2 15 | cosTDP KEYWORD2 16 | polyTDP KEYWORD2 17 | DayofYear KEYWORD2 18 | halfTime KEYWORD2 19 | getRiseSet KEYWORD2 20 | baseD KEYWORD2 21 | baseN KEYWORD2 22 | getTPeak KEYWORD2 23 | getDOYRange KEYWORD2 24 | calcSun KEYWORD2 25 | calcTDP KEYWORD2 26 | 27 | 28 | -------------------------------------------------------------------------------- /library.properties: -------------------------------------------------------------------------------- 1 | name=Norman 2 | version=1.0.0 3 | author=934Virginia <934Virginia@gmail.com> 4 | maintainer=934Virginia <934Virginia@gmail.com> 5 | sentence=Mathematically simulate natural cycles of daylight, temperature, and humidity for remote locations using minimal data sets. 6 | paragraph=Given target coordinates, specified date ranges, and minimum/maximum values for temperature and humidity at target's annual extremes, generate time_t values for sunrise and sunset, and floating point numbers for temperature, dew point, and relative humidity. Requires Dusk2Dawn library by DM Kishi; an adapted C++ port of NOAA's Solar Calculator. 7 | category=Other 8 | url=https://github.com/934virginia/Norman 9 | architectures=* 10 | includes=Dusk2Dawn.h,time.h 11 | depends=Dusk2Dawn 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /Norman.h: -------------------------------------------------------------------------------- 1 | /* Norman.h - Library for climatic simulation. 2 | Created by 934Virginia, 2019-07-08. 3 | Released into the public domain. 4 | Dusk2Dawn library, NOAA Solar Calculator, and DayofYear function are the property of their respective authors. 5 | */ 6 | #ifndef Norman_h 7 | #define Norman_h 8 | 9 | #include "Arduino.h" 10 | #include "Dusk2Dawn.h" 11 | #include 12 | 13 | struct SetTarget_Origin { 14 | float Lat; 15 | float Lon; 16 | int TZ; 17 | time_t StartDT; 18 | int SLength; 19 | }; 20 | struct SetRangeTDP { 21 | time_t xminL; 22 | time_t xmaxL; 23 | float minlow; 24 | float minhigh; 25 | float maxlow; 26 | float maxhigh; 27 | }; 28 | 29 | 30 | //Norman(); 31 | //struct SetTarget_Origin(float, float, int, time_t, int); 32 | //struct SetRangeTDP(time_t, time_t, float, float, float, float); 33 | time_t getTime(int, byte, byte, byte, byte, byte); 34 | bool DSTCheck(time_t); 35 | float RHtoDP(float, float, String); 36 | time_t getSun(time_t, struct SetTarget_Origin, struct SetTarget_Origin, String); 37 | float getTDP(time_t, struct SetTarget_Origin, struct SetTarget_Origin, struct SetRangeTDP, int); 38 | float getRH(float, float, String); 39 | 40 | 41 | 42 | int getYear(time_t); 43 | byte getMonth(time_t); 44 | byte getDay(time_t); 45 | byte getHour(time_t); 46 | byte getMinute(time_t); 47 | byte getSecond(time_t); 48 | float cosTDP(float, float, float, float, float); 49 | float polyTDP(float, float, float, float, float); 50 | int DayofYear(int, int, int); 51 | time_t halfTime(time_t, time_t); 52 | time_t getRiseSet(float, float, int, float, float, int, time_t, time_t, int, int, String, int); 53 | float baseD(float, float, float, float, time_t, time_t, time_t, time_t, int, long, int, int, float, int); 54 | float baseN(float, float, float, float, time_t, time_t, time_t, time_t, int, long, int, int, time_t, int); 55 | long getTPeak(time_t, time_t, int); 56 | int getDOYRange(int, int, time_t); 57 | time_t calcSun(time_t, float, float, int, float, float, int, time_t, time_t, int, String); 58 | float calcTDP(time_t, float, float, int, float, float, int, time_t, time_t, int, time_t, time_t, float, float, float, float, int); 59 | 60 | #endif 61 | -------------------------------------------------------------------------------- /examples/Example_Test/Example_Test.ino: -------------------------------------------------------------------------------- 1 | //digitalClockLog and logDigits functions modified from example sketches included in RTCLib library created by Adafruit. 2 | //All other functions and methods created by 934Virginia, 2019-07-08, and released into the public domain. 3 | 4 | #include 5 | 6 | #include 7 | #include "Norman.h" 8 | 9 | //Outputs a formatted year's worth of data for graphical comparison to real-world data. 10 | //Just copy-paste the content of the serial monitor to a .csv, and you're good to go. 11 | 12 | void digitalClockLog(time_t Xtime) { 13 | // digital clock display of the time 14 | //Formatted to match NCDC timestamps for climatic normals. 15 | 16 | //logDigits(getYear(Xtime)); 17 | //Serial.print('-'); 18 | logDigits(getMonth(Xtime)); 19 | Serial.print('-'); 20 | logDigits(getDay(Xtime)); 21 | Serial.print("T"); 22 | logDigits(getHour(Xtime)); 23 | Serial.print(':'); 24 | logDigits(getMinute(Xtime)); 25 | Serial.print(':'); 26 | logDigits(getSecond(Xtime)); 27 | } 28 | 29 | void logDigits(int digits) { 30 | // utility function for digital clock display: prints preceding colon and leading 0 31 | if (digits < 10) 32 | Serial.print('0'); 33 | Serial.print(digits); 34 | } 35 | 36 | ////////////////////////////////////STRUCT INPUT//////////////////////////////////////////////////////////////// 37 | //SetTarget_Origin Structure is (Latitude, Longitude, Timezone(standard time), Season Start, Season Length). 38 | //Season Length is unused for Origin parameters. 39 | 40 | //SetRangeTDP Structure is (coldest day of year, hottest day of year, Minimum value on coldest day, Maximum value on coldest day, Minimum value on hottest day, Maximum value on hottest day). 41 | //if you can't find minimum and maximum Dew Point values for your location on the hottest and coldest days of the year, 42 | //you'll need to use the RHtoDP(Temp, RH, "F") function for the temperature and RH data at those points in time. 43 | ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// 44 | 45 | //Denver 46 | struct SetTarget_Origin Target = {39.7392, -104.9903, -7, getTime(2010, 1, 1, 0, 0, 0), 365}; 47 | struct SetTarget_Origin Origin = {39.7392, -104.9903, -7, getTime(2010, 1, 1, 0, 0, 0), 365}; 48 | struct SetRangeTDP Temperature = {getTime(2010, 12, 30, 0, 0, 0), getTime(2010, 7, 10, 0, 0, 0), 24.8, 39.0, 60.8, 85.1}; 49 | struct SetRangeTDP DewPoint = {getTime(2010, 12, 30, 0, 0, 0), getTime(2010, 7, 10, 0, 0, 0), 12.9, 16.7, 42.2, 48.6}; 50 | 51 | 52 | time_t currentTime = getTime(2010, 1, 1, 0, 0, 0); 53 | 54 | 55 | void setup() { 56 | Serial.begin(9600); 57 | Serial.print("\""); 58 | Serial.print("Dateime"); 59 | Serial.print("\",\""); 60 | Serial.print("Temperature F"); 61 | Serial.print("\",\""); 62 | Serial.print("Dewpoint F"); 63 | Serial.print("\",\""); 64 | Serial.print("Percent Relative Humidity"); 65 | Serial.println("\""); 66 | 67 | } 68 | 69 | void loop() { 70 | 71 | currentTime += 3600; 72 | 73 | /* 74 | //If you want to print whether or not the sun is up, uncomment this section. This messes up the formatting for comparisons, though. 75 | time_t testoRise = getSun(currentTime, Target, Origin, "Rise"); 76 | time_t testoSet = getSun(currentTime, Target, Origin, "Set"); 77 | 78 | if (currentTime >= testoRise && currentTime < testoSet) { 79 | Serial.println("DAY"); 80 | } 81 | else { 82 | Serial.println("NIGHT"); 83 | } 84 | */ 85 | 86 | //Get your projections 87 | float testoT = getTDP(currentTime, Target, Origin, Temperature, 150); 88 | float testoDP = getTDP(currentTime, Target, Origin, DewPoint, 150); 89 | float testoRH = getRH(testoT, testoDP, "F"); 90 | //float testoRH = getRH(testoT, testoDP, "C"); 91 | 92 | //Silly way to ensure it stops printing the values after a year. 93 | if (getYear(currentTime) == 2010) 94 | { 95 | Serial.print("\""); 96 | digitalClockLog(currentTime); 97 | Serial.print("\",\""); 98 | Serial.print(testoT); 99 | Serial.print("\",\""); 100 | Serial.print(testoDP); 101 | Serial.print("\",\""); 102 | Serial.print(testoRH); 103 | Serial.println("\""); 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /parameters.txt: -------------------------------------------------------------------------------- 1 | 2 | //Bangor// 3 | struct SetTarget_Origin Target = {44.8016, -68.7712, -5, getTime(2010, 1, 1, 0, 0, 0), 365}; 4 | struct SetTarget_Origin Origin = {44.8016, -68.7712, -5, getTime(2010, 1, 1, 0, 0, 0), 365}; 5 | struct SetRangeTDP Temperature = {getTime(2010, 1, 29, 0, 0, 0), getTime(2010, 7, 27, 0, 0, 0), 13.0, 25.8, 60.9, 78.0}; 6 | struct SetRangeTDP DewPoint = {getTime(2010, 1, 29, 0, 0, 0), getTime(2010, 7, 27, 0, 0, 0), 6.6, 11.8, 57.7, 60.0}; 7 | 8 | 9 | //Denver// 10 | struct SetTarget_Origin Target = {39.7392, -104.9903, -7, getTime(2010, 1, 1, 0, 0, 0), 365}; 11 | struct SetTarget_Origin Origin = {39.7392, -104.9903, -7, getTime(2010, 1, 1, 0, 0, 0), 365}; 12 | struct SetRangeTDP Temperature = {getTime(2010, 12, 30, 0, 0, 0), getTime(2010, 7, 10, 0, 0, 0), 24.8, 39.0, 60.8, 85.1}; 13 | struct SetRangeTDP DewPoint = {getTime(2010, 12, 30, 0, 0, 0), getTime(2010, 7, 10, 0, 0, 0), 12.9, 16.7, 42.2, 48.6}; 14 | 15 | 16 | //Fairbanks// 17 | struct SetTarget_Origin Target = {64.8378, -147.7164, -9, getTime(2010, 1, 1, 0, 0, 0), 365}; 18 | struct SetTarget_Origin Origin = {64.8378, -147.7164, -9, getTime(2010, 1, 1, 0, 0, 0), 365}; 19 | struct SetRangeTDP Temperature = {getTime(2010, 1, 18, 0, 0, 0), getTime(2010, 7, 3, 0, 0, 0), -5.9, -2.8, 55.2, 71.7}; 20 | struct SetRangeTDP DewPoint = {getTime(2010, 1, 18, 0, 0, 0), getTime(2010, 7, 3, 0, 0, 0), -7.4, -6.4, 47.1, 50.0}; 21 | 22 | 23 | //Helena// 24 | struct SetTarget_Origin Target = {46.5891, -112.0391, -7, getTime(2010, 1, 1, 0, 0, 0), 365}; 25 | struct SetTarget_Origin Origin = {46.5891, -112.0391, -7, getTime(2010, 1, 1, 0, 0, 0), 365}; 26 | struct SetRangeTDP Temperature = {getTime(2010, 1, 1, 0, 0, 0), getTime(2010, 7, 27, 0, 0, 0), 15.6, 25.7, 57.2, 84.1}; 27 | struct SetRangeTDP DewPoint = {getTime(2010, 1, 1, 0, 0, 0), getTime(2010, 7, 27, 0, 0, 0), 9.2, 14.0, 42.9, 46.8}; 28 | 29 | 30 | //Homer// 31 | struct SetTarget_Origin Target = {59.6425, -151.5483, -9, getTime(2010, 1, 1, 0, 0, 0), 365}; 32 | struct SetTarget_Origin Origin = {59.6425, -151.5483, -9, getTime(2010, 1, 1, 0, 0, 0), 365}; 33 | struct SetRangeTDP Temperature = {getTime(2010, 1, 17, 0, 0, 0), getTime(2010, 7, 30, 0, 0, 0), 25.2, 28.9, 50.6, 59.3}; 34 | struct SetRangeTDP DewPoint = {getTime(2010, 1, 17, 0, 0, 0), getTime(2010, 7, 30, 0, 0, 0), 19.9, 22.2, 47.8, 50.3}; 35 | 36 | //Lincoln// 37 | struct SetTarget_Origin Target = {40.8136, -96.7026, -6, getTime(2010, 1, 1, 0, 0, 0), 365}; 38 | struct SetTarget_Origin Origin = {40.8136, -96.7026, -6, getTime(2010, 1, 1, 0, 0, 0), 365}; 39 | struct SetRangeTDP Temperature = {getTime(2010, 1, 13, 0, 0, 0), getTime(2010, 7, 20, 0, 0, 0), 18.6, 32.4, 68.4, 87.7}; 40 | struct SetRangeTDP DewPoint = {getTime(2010, 1, 13, 0, 0, 0), getTime(2010, 7, 20, 0, 0, 0), 13.2, 17.4, 63.6, 66.2}; 41 | 42 | //Miami// 43 | struct SetTarget_Origin Target = {25.7617, -80.1918, -5, getTime(2010, 1, 1, 0, 0, 0), 365}; 44 | struct SetTarget_Origin Origin = {25.7617, -80.1918, -5, getTime(2010, 1, 1, 0, 0, 0), 365}; 45 | struct SetRangeTDP Temperature = {getTime(2010, 1, 17, 0, 0, 0), getTime(2010, 8, 9, 0, 0, 0), 61.6, 73.7, 79.4, 88.3}; 46 | struct SetRangeTDP DewPoint = {getTime(2010, 1, 17, 0, 0, 0), getTime(2010, 8, 9, 0, 0, 0), 56.1, 57.6, 73.4, 75.1}; 47 | 48 | //Richmond// 49 | struct SetTarget_Origin Target = {37.5407, -77.4360, -5, getTime(2010, 1, 1, 0, 0, 0), 365}; 50 | struct SetTarget_Origin Origin = {37.5407, -77.4360, -5, getTime(2010, 1, 1, 0, 0, 0), 365}; 51 | struct SetRangeTDP Temperature = {getTime(2010, 1, 29, 0, 0, 0), getTime(2010, 7, 21, 0, 0, 0), 31.8, 46.1, 71.1, 87.5}; 52 | struct SetRangeTDP DewPoint = {getTime(2010, 1, 29, 0, 0, 0), getTime(2010, 7, 21, 0, 0, 0), 25.0, 26.6, 67.9, 69.5}; 53 | 54 | //San Diego// 55 | struct SetTarget_Origin Target = {32.7157, -117.1611, -8, getTime(2010, 1, 1, 0, 0, 0), 365}; 56 | struct SetTarget_Origin Origin = {32.7157, -117.1611, -8, getTime(2010, 1, 1, 0, 0, 0), 365}; 57 | struct SetRangeTDP Temperature = {getTime(2010, 12, 28, 0, 0, 0), getTime(2010, 8, 26, 0, 0, 0), 49.9, 63.2, 67.8, 75.9}; 58 | struct SetRangeTDP DewPoint = {getTime(2010, 12, 28, 0, 0, 0), getTime(2010, 8, 26, 0, 0, 0), 39.6, 46.5, 62.5, 63.5}; 59 | 60 | //Seattle// 61 | struct SetTarget_Origin Target = {47.6062, -122.3321, -8, getTime(2010, 1, 1, 0, 0, 0), 365}; 62 | struct SetTarget_Origin Origin = {47.6062, -122.3321, -8, getTime(2010, 1, 1, 0, 0, 0), 365}; 63 | struct SetRangeTDP Temperature = {getTime(2010, 12, 23, 0, 0, 0), getTime(2010, 8, 1, 0, 0, 0), 37.5, 42.4, 57.3, 75.6}; 64 | struct SetRangeTDP DewPoint = {getTime(2010, 12, 23, 0, 0, 0), getTime(2010, 8, 1, 0, 0, 0), 32.8, 34.7, 52.9, 54.1}; 65 | 66 | //Shreveport// 67 | struct SetTarget_Origin Target = {32.5252, -93.7502, -6, getTime(2010, 1, 1, 0, 0, 0), 365}; 68 | struct SetTarget_Origin Origin = {32.5252, -93.7502, -6, getTime(2010, 1, 1, 0, 0, 0), 365}; 69 | struct SetRangeTDP Temperature = {getTime(2010, 1, 8, 0, 0, 0), getTime(2010, 8, 12, 0, 0, 0), 39.9, 54.8, 73.8, 91.7}; 70 | struct SetRangeTDP DewPoint = {getTime(2010, 1, 8, 0, 0, 0), getTime(2010, 8, 12, 0, 0, 0), 34.9, 36.6, 69.2, 72.0}; 71 | 72 | //Tucson 73 | struct SetTarget_Origin Target = {32.2226, -110.9747, -7, getTime(2010, 1, 1, 0, 0, 0), 365}; 74 | struct SetTarget_Origin Origin = {32.2226, -110.9747, -7, getTime(2010, 1, 1, 0, 0, 0), 365}; 75 | struct SetRangeTDP Temperature = {getTime(2010, 1, 4, 0, 0, 0), getTime(2010, 6, 29, 0, 0, 0), 42.1, 64.2, 75.1, 100.9}; 76 | struct SetRangeTDP DewPoint = {getTime(2010, 1, 4, 0, 0, 0), getTime(2010, 6, 29, 0, 0, 0), 28.9, 31.5, 42.1, 47.3}; 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Norman 2 | 3 | Arduino Library for mathematical climatic simulation of natural daylight, temperature, and humidity cycles using minimal data sets. 4 | 5 | Given target coordinates, specified date ranges, and minimum/maximum values for temperature and humidity at target's annual extremes, generate time_t values for sunrise and sunset, and floating point numbers for temperature, dew point, and relative humidity. Requires [Dusk2Dawn library by DM Kishi](https://github.com/dmkishi/Dusk2Dawn); an adapted C++ port of the [NOAA Solar Calculator](https://www.esrl.noaa.gov/gmd/grad/solcalc/). 6 | 7 | ## Licensing Disclaimer 8 | Raw climatic data used in this project, as well as original Solar Calculator software provided by NOAA ESRL Global Monitoring Division, Boulder, Colorado, USA (http://esrl.noaa.gov/gmd/) 9 | 10 | All dependencies referenced in this repository are the properties of their respective authors, and are exempt from the the terms of this project's governing intellectual property license (the terms of which can be found in the "LICENSE" file located under the master branch of this repository) unless otherwise stated in the terms of the respective licenses and copyright statutes governing the use and distribution of said dependencies. 11 | 12 | ## Installation 13 | 14 | ### From the Library Manager 15 | 1. Launch the Arduino IDE and navigate to Sketch → Include Library → Manage Libraries. 16 | 2. In the library manager, scroll to Norman or enter the name into the search field. 17 | 3. Click on the library, then click on the Install button. 18 | 19 | ### From the ZIP file 20 | 1. Download the [ZIP file](https://github.com/934Virginia/Norman/archive/master.zip). 21 | 2. Launch the Arduino IDE and navigate to *Sketch → Include Library → Add .ZIP Library...*. From the prompt, select the ZIP just downloaded. 22 | 23 | ## Usage 24 | Included example scripts can be utilized to either project formatted data, or control climatic conditions using RTCLib, Adafruit_Sensor, DHT, and DHT_U libraries created by Adafruit. 25 | Example datasets for various US cities included in parameters.txt file. Just copy-paste them into the Example_Test script to get annual data. 26 | 27 | ## FAQ 28 | ### What is this? 29 | 30 | This is Norman. Norman is a climatic simulation library that mathematically projects natural cycles of daylight, temperature, and humidity for remote locations (and periods of time) using minimal data sets. It was designed to handle climate control for indoor/greenhouse agriculture and botany applications when paired with an RTC module. It was built in the Arduino IDE and tested on an ATmega328P. It was named for Norman Borlaug; agronomist of legend and friend to all who enjoy not starving to death. 31 | 32 | ### How does it work? 33 | #### Daylight 34 | Norman makes extensive use of the Dusk2Dawn library, by DM Kishi. It relies on Dusk2Dawn to retrieve daily times for sunrise and sunset based on latitude, longitude, and timezone of the target location. Optionally, it anchors these daily light cycles to solar noon of local daylight schedules. This enables users utilizing a greenhouse to incorporate photoresistors and PWM driver control for the purposes of energy-efficient supplemental lighting if so desired. 35 | 36 | #### Temperature and Humidity 37 | Yearly temperature and dew point trends can be loosely expressed by a sine wave in most climates. This means we can project daily minima and maxima as long as we have minimum and maximum values for the hottest and coldest days of the year, as well as the indexed locations of these days for the target location. Seasonal lag was loosely accounted for by modifying the width of this sine wave based on the number of days between the hottest and coldest days of the year (relative to the current time). Daily patterns for temperature and dew point were more complicated. I used a polynomial function for daytime shifts, an exponential function for nightly decay, and a regressive anchoring system in order to create smooth transitions between day and night cycles while continuing to follow the annual sine wave. Relative humidity is retrieved using these temperature and dew point projections. This enables users to incorporate these projected values into climate control applications using popular temperature and humidity sensors (DHT11, DHT22, BMP280, etc.). 38 | 39 | ### Does it only calculate values for the current day? 40 | No. It can be programmed to mimic conditions for any time of the year, from any time of the year. Optionally, users can designate the length and starting point of a "season," and repeat the projected conditions of this time period over and over again without having to re-flash the sketch. This method retains the (optional) functionality of anchoring target daylight cycles to local cycles happening in real time. 41 | 42 | ### How accurate is it? 43 | That's a complicated question. You can see for yourself at [my plot.ly page](https://plot.ly/~934Virginia). I tested a dozen US cities to see how my projections would stack up, and I think it did alright, considering how crude it is. Seems to do a lot better in coastal and continental climates than mountains and deserts. I've got some ideas of how to improve its consistency, but addressing those issues would necessitate the time and labor of people who actually know what they're doing. As it is, I'm ready to move on to other projects, so we're calling it as done as done gets. 44 | 45 | ### Why are you writing all this? 46 | Because I hope that my dumb little project will inflame the curiosity of smart people. I want other people to do this better, because I think a comprehensive mathematical model that can accurately simulate remote climatic conditions might have some exciting implications for agricultural development and botany. If you are interested in taking a swing at it, contact me. I can put you onto some questions and observations that might help you get started. Probably the first order of business would be whipping up some sunrise and sunset functions that build and fade naturally based on solar elevation and azimuth. It would also be rad if someone sent me a picture of them growing passion fruit in Siberia using my garbage code and a $10 microcontroller. 47 | 48 | ## Known Bugs 49 | (Under construction!) 50 | 51 | ## History 52 | - **2019-7-8**: Released. 53 | -------------------------------------------------------------------------------- /examples/Example_Practical/Example_Practical.ino: -------------------------------------------------------------------------------- 1 | // All included libraries and related code snippets extracted and modified from example sketches included in said libraries are the property of their respective authors. 2 | //getStartDTO and LTime functions created by 934Virginia, 2019-07-08, and released into the public domain. 3 | 4 | #include 5 | #include "RTClib.h" 6 | #include 7 | #include "Norman.h" 8 | #include 9 | #include 10 | #include 11 | 12 | #define DHTPIN 2 // Pin which is connected to the DHT sensor. 13 | 14 | // Uncomment the type of sensor in use: 15 | //#define DHTTYPE DHT11 // DHT 11 16 | #define DHTTYPE DHT22 // DHT 22 (AM2302) 17 | //#define DHTTYPE DHT21 // DHT 21 (AM2301) 18 | 19 | 20 | DHT_Unified dht(DHTPIN, DHTTYPE); 21 | 22 | //Type of RTC being used. I highly recommend the DS3231 for long-term accuracy. 23 | RTC_DS3231 rtc; 24 | 25 | 26 | //Define pins responsible for controlling specific relays. Can define rf codes similarly if going that route.// 27 | int Lights = 7; 28 | 29 | int TempPlus = 8; 30 | int TempMinus = 9; 31 | int RHPlus = 10; 32 | int RHMinus = 11; 33 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////// 34 | 35 | //Declare how close to keep real-world climate conditions collected by the sensor to our mathematically projected values 36 | //(Target range = projected value +- this integer.) 37 | //The smaller the integer, the more precisely your devices will attempt to control your environment. 38 | int Precision = 2; 39 | 40 | //Conversion to make RTC DT values play nice with our calculations 41 | time_t LTime(DateTime x) { 42 | int yearx = x.year(); 43 | byte monthx = x.month(); 44 | byte dayx = x.day(); 45 | byte hourx = x.hour(); 46 | byte minutex = x.minute(); 47 | byte secondx = x.second(); 48 | 49 | struct tm tmSet; 50 | tmSet.tm_year = yearx - 1900; 51 | tmSet.tm_mon = monthx - 1; 52 | tmSet.tm_mday = dayx; 53 | tmSet.tm_hour = hourx; 54 | tmSet.tm_min = minutex; 55 | tmSet.tm_sec = secondx; 56 | return mk_gmtime(&tmSet) + UNIX_OFFSET; 57 | } 58 | 59 | void digitalClockLog(time_t Xtime) { 60 | // digital clock display of the time 61 | logDigits(getYear(Xtime)); 62 | Serial.print('-'); 63 | logDigits(getMonth(Xtime)); 64 | Serial.print('-'); 65 | logDigits(getDay(Xtime)); 66 | Serial.print("T"); 67 | logDigits(getHour(Xtime)); 68 | Serial.print(':'); 69 | logDigits(getMinute(Xtime)); 70 | Serial.print(':'); 71 | logDigits(getSecond(Xtime)); 72 | } 73 | 74 | void logDigits(int digits) { 75 | // utility function for digital clock display: prints preceding colon and leading 0 76 | if (digits < 10) 77 | Serial.print('0'); 78 | Serial.print(digits); 79 | } 80 | 81 | //Sets the start date of the first "season" to the date we flashed the sketch, and adjusts for DST. 82 | //If you flash between midnight and 1am during DST, expect the season to start the day before. 83 | time_t getStartDTO() { 84 | if (DSTCheck(LTime(DateTime(F(__DATE__), F(__TIME__)))) == true) { 85 | time_t StartDTOadj = LTime(DateTime(F(__DATE__), F(__TIME__)) - TimeSpan(0, 1, 0, 0)); 86 | return getTime(getYear(StartDTOadj), getMonth(StartDTOadj), getDay(StartDTOadj), 0, 0, 0); 87 | } 88 | else { 89 | time_t StartDTOx = LTime(DateTime(F(__DATE__), F(__TIME__))); 90 | return getTime(getYear(StartDTOx), getMonth(StartDTOx), getDay(StartDTOx), 0, 0, 0); 91 | } 92 | } 93 | time_t StartDTO = getStartDTO(); 94 | 95 | 96 | ////////////////////////////////////STRUCT INPUT//////////////////////////////////////////////////////////////// 97 | //SetTarget_Origin Structure = (Latitude, Longitude, Timezone(standard time), Season Start, Season Length). 98 | //Season Length is unused for Origin parameters. 99 | 100 | //SetRangeTDP Structure = (coldest day of year, hottest day of year, Minimum value on coldest day, Maximum value on coldest day, Minimum value on hottest day, Maximum value on hottest day). 101 | //if you can't find minimum and maximum Dew Point values for your location on the hottest and coldest days of the year, 102 | //you'll need to use the RHtoDP(Temp, RH, "F") function for the temperature and RH data at those points in time. 103 | ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// 104 | 105 | 106 | 107 | //!Origin Season Start MUST be changed from copied parameter value to StartDTO in order to sync with RTC! 108 | 109 | //Changing Origin Lat, Lon, and TZ values to your own local values will center projected sunrise and sunset times on your own solar noon. 110 | //Use this to tinker with supplemental lighting setups, or skip entering your local values altogether. 111 | 112 | ////////////////////////////////////////////////////// 113 | 114 | //San Diego// 115 | struct SetTarget_Origin Target = {32.7157, -117.1611, -8, getTime(2019, 4, 15, 0, 0, 0), 90}; 116 | struct SetTarget_Origin Origin = {32.7157, -117.1611, -8, StartDTO, 365}; 117 | struct SetRangeTDP Temperature = {getTime(2010, 12, 28, 0, 0, 0), getTime(2010, 8, 26, 0, 0, 0), 49.9, 63.2, 67.8, 75.9}; 118 | struct SetRangeTDP DewPoint = {getTime(2010, 12, 28, 0, 0, 0), getTime(2010, 8, 26, 0, 0, 0), 39.6, 46.5, 62.5, 63.5}; 119 | 120 | ////////////////////////////////////////////////////// 121 | 122 | 123 | time_t currentTime; 124 | 125 | void setup() { 126 | Serial.begin(9600); 127 | Serial.println("--------------------------------------------------"); 128 | 129 | if (! rtc.begin()) { 130 | Serial.println("Couldn't find RTC"); 131 | while (1); 132 | } 133 | 134 | if (rtc.lostPower()) { 135 | Serial.println("RTC lost power, lets set the time!"); 136 | //The following sets the RTC to the date & time this sketch was compiled; runs a check to see if it was flashed during DST, and adjusts it to standard time if necessary. 137 | //If you live somewhere like Arizona or in a country with unique DST rules, you'll need to tinker with this, along with getStartDTO function. 138 | DateTime rtcDT; 139 | if (DSTCheck(LTime(DateTime(F(__DATE__), F(__TIME__)))) == true) { 140 | rtcDT = DateTime(F(__DATE__), F(__TIME__)) - TimeSpan(0, 1, 0, 0); 141 | } 142 | rtc.adjust(rtcDT); 143 | } 144 | 145 | // Initialize sensor. 146 | dht.begin(); 147 | sensor_t sensor; 148 | dht.temperature().getSensor(&sensor); 149 | 150 | //Ready up your relay output pins. 151 | pinMode(Lights, OUTPUT); 152 | pinMode(TempPlus, OUTPUT); 153 | pinMode(TempMinus, OUTPUT); 154 | pinMode(RHPlus, OUTPUT); 155 | pinMode(RHMinus, OUTPUT); 156 | 157 | } 158 | 159 | void loop() { 160 | 161 | 162 | currentTime = LTime(rtc.now()); 163 | 164 | sensors_event_t event; 165 | dht.temperature().getEvent(&event); 166 | 167 | float TempReal = (float(event.temperature) * 1.8 + 32); 168 | //Use this instead if measuring in Celsius. 169 | //int TempReal = int(event.temperature); 170 | 171 | dht.humidity().getEvent(&event); 172 | float RHReal = float(event.relative_humidity); 173 | 174 | 175 | time_t projRise = getSun(currentTime, Target, Origin, "Rise"); 176 | time_t projSet = getSun(currentTime, Target, Origin, "Set"); 177 | 178 | Serial.print("Current Time: "); 179 | digitalClockLog(currentTime); 180 | Serial.println(); 181 | 182 | Serial.print("Sunrise: "); 183 | digitalClockLog(projRise); 184 | Serial.println(); 185 | Serial.print("Sunset: "); 186 | digitalClockLog(projSet); 187 | Serial.println(); 188 | 189 | 190 | if (currentTime >= projRise && currentTime < projSet) { 191 | Serial.println("Lights ON"); 192 | digitalWrite(Lights, HIGH); 193 | } 194 | else { 195 | Serial.println("Lights OFF"); 196 | digitalWrite(Lights, LOW); 197 | } 198 | 199 | 200 | //Structure is Current Time, Target location values (SetTarget_Origin), Origin location values (SetTarget_Origin), Numerical parameters (SetRangeTDP), Daily thermal peak (hottest time of the day, expressed in minutes after solar noon). 201 | //Thermal peak is a really weird piece of this project. 150 will work fine for most projections, but I built in explicit front-end access so that people can fiddle with it and plug in a proper algorithm. 202 | //TL;DR: Leave it at 150 unless you're a solar physicist. 203 | float TempProj = getTDP(currentTime, Target, Origin, Temperature, 150); 204 | float DPProj = getTDP(currentTime, Target, Origin, DewPoint, 150); 205 | float RHProj = getRH(TempProj, DPProj, "F"); 206 | //float RHProj = getRH(TempProj, DPProj, "C"); 207 | 208 | Serial.print("Actual temperature): "); 209 | Serial.print(TempReal); 210 | Serial.println(" F"); 211 | //Serial.println(" C"); 212 | 213 | Serial.print("Projected temperature: "); 214 | Serial.print(TempProj); 215 | Serial.println(" F"); 216 | //Serial.println(" C"); 217 | 218 | 219 | if (TempReal > (TempProj + Precision)) 220 | { 221 | digitalWrite(TempMinus, HIGH); 222 | digitalWrite(TempPlus, LOW); 223 | Serial.println("High temperature! Cooling environment."); 224 | } 225 | else if (TempReal < (TempProj - Precision)) 226 | { 227 | digitalWrite(TempPlus, HIGH); 228 | digitalWrite(TempMinus, LOW); 229 | Serial.println("Low temperature! Heating environment."); 230 | } 231 | 232 | Serial.print("Actual RH: "); 233 | Serial.print(RHReal); 234 | Serial.println("%"); 235 | 236 | Serial.print("Projected RH: "); 237 | Serial.print(RHProj); 238 | Serial.println("%"); 239 | 240 | if (RHReal > (RHProj + Precision)) 241 | { 242 | digitalWrite(RHMinus, HIGH); 243 | digitalWrite(RHPlus, LOW); 244 | Serial.println("High humidity! Dehumidifying environment."); 245 | } 246 | else if (RHReal < (RHProj - Precision)) 247 | { 248 | digitalWrite(RHPlus, HIGH); 249 | digitalWrite(RHMinus, LOW); 250 | Serial.println("Low humidity! Humidifying environment."); 251 | } 252 | Serial.println("--------------------------------------------------"); 253 | 254 | //Adjust this delay to control frequency of readings and control operations. 255 | //Another option is to use TimeAlarms, but that gets fiddly with avr time functions. 256 | delay(10000); 257 | } 258 | -------------------------------------------------------------------------------- /Norman.cpp: -------------------------------------------------------------------------------- 1 | #include "Arduino.h" 2 | #include 3 | #include "Norman.h" 4 | #include "Dusk2Dawn.h" 5 | 6 | 7 | 8 | 9 | 10 | 11 | //Unused as of now, but included in case someone needs to interact with syncprovider 12 | /* 13 | time_t syncProvider() { 14 | DateTime x = rtc.now(); 15 | 16 | int yearx = x.year(); 17 | byte monthx = x.month(); 18 | byte dayx = x.day(); 19 | byte hourx = x.hour(); 20 | byte minutex = x.minute(); 21 | byte secondx = x.second(); 22 | 23 | struct tm tmSet; 24 | tmSet.tm_year = yearx - 1900; 25 | tmSet.tm_mon = monthx - 1; 26 | tmSet.tm_mday = dayx; 27 | tmSet.tm_hour = hourx; 28 | tmSet.tm_min = minutex; 29 | tmSet.tm_sec = secondx; 30 | return mk_gmtime(&tmSet) + UNIX_OFFSET; 31 | } 32 | */ 33 | 34 | 35 | 36 | //creates time_t object 37 | time_t getTime(int yearx, byte monthx, byte dayx, byte hourx, byte minutex, byte secondx) { 38 | 39 | struct tm tmSet; 40 | tmSet.tm_year = yearx - 1900; 41 | tmSet.tm_mon = monthx - 1; 42 | tmSet.tm_mday = dayx; 43 | tmSet.tm_hour = hourx; 44 | tmSet.tm_min = minutex; 45 | tmSet.tm_sec = secondx; 46 | return mk_gmtime(&tmSet) + UNIX_OFFSET; 47 | } 48 | 49 | int getYear(time_t timeIn) { 50 | timeIn -= UNIX_OFFSET; 51 | struct tm * getStruct; 52 | getStruct = gmtime(&timeIn); 53 | 54 | return (getStruct->tm_year) + 1900; 55 | } 56 | 57 | byte getMonth(time_t timeIn) { 58 | timeIn -= UNIX_OFFSET; 59 | struct tm * getStruct; 60 | getStruct = gmtime(&timeIn); 61 | 62 | return (getStruct->tm_mon) + 1; 63 | } 64 | 65 | byte getDay(time_t timeIn) { 66 | timeIn -= UNIX_OFFSET; 67 | struct tm * getStruct; 68 | getStruct = gmtime(&timeIn); 69 | 70 | return (getStruct->tm_mday); 71 | } 72 | 73 | byte getHour(time_t timeIn) { 74 | timeIn -= UNIX_OFFSET; 75 | struct tm * getStruct; 76 | getStruct = gmtime(&timeIn); 77 | 78 | return (getStruct->tm_hour); 79 | } 80 | 81 | byte getMinute(time_t timeIn) { 82 | timeIn -= UNIX_OFFSET; 83 | struct tm * getStruct; 84 | getStruct = gmtime(&timeIn); 85 | 86 | return (getStruct->tm_min); 87 | } 88 | 89 | byte getSecond(time_t timeIn) { 90 | timeIn -= UNIX_OFFSET; 91 | struct tm * getStruct; 92 | getStruct = gmtime(&timeIn); 93 | 94 | return (getStruct->tm_sec); 95 | } 96 | 97 | //used to get daily lows and highs from lows and highs of hottest and coldest days of the year 98 | float cosTDP(float x, float ymin, float ymax, float xmin, float cycle) { 99 | return (((ymax + ymin) / 2) - (((ymax - ymin) / 2) * cos((((2 * PI) / cycle) * x) - (((2 * PI) / cycle) * xmin)))); 100 | } 101 | 102 | //our base function for the daytime temperature and dewpoint shifts 103 | float polyTDP(float x, float ymin, float ymax, float xmin, float cycle) { 104 | return ((pow((x - xmin), 2)) * ((1 / (pow((cycle / 2), 4) / (ymax - ymin))) * (pow((x - cycle), 2))) + ymin); 105 | } 106 | 107 | //alternate daytime function-- the curvature is a bit closer to raw data, but there's no exponential ramp up from dawn doing it this way. Also throws baseN off. 108 | //worth playing with if you want to rework things. 109 | /*float paraTDP(float x, float ymin, float ymax, float xmin, float cycle) { 110 | return (ymax - (((ymax - ymin) / (cycle / 2)) / (cycle / 2)) * pow((((x - xmin) - (cycle / 2))), 2)); 111 | }*/ 112 | 113 | 114 | //"toDayOfYear.ino" function swiped from jrleeman. Returns numerical day of calendar year. 115 | int DayofYear(int dayx, int monthx, int yearx) { 116 | 117 | int daysInMonth[] = {31,28,31,30,31,30,31,31,30,31,30,31}; 118 | 119 | // Verify we got a 4-digit year 120 | if (yearx < 1000 || dayx < 1 || dayx > daysInMonth[monthx-1]) { 121 | return 999; 122 | } 123 | 124 | if (yearx%4 == 0) { 125 | if (yearx%100 != 0) { 126 | daysInMonth[1] = 29; 127 | } 128 | else { 129 | if (yearx%400 == 0) { 130 | daysInMonth[1] = 29; 131 | } 132 | } 133 | } 134 | 135 | int doy = 0; 136 | for (int i = 0; i < monthx - 1; i++) { 137 | doy += daysInMonth[i]; 138 | } 139 | 140 | doy += dayx; 141 | return doy; 142 | } 143 | 144 | 145 | //Some of this stuff is probably super redundant. 146 | 147 | 148 | time_t halfTime(time_t one, time_t two) { 149 | if (two > one) { 150 | return one + (long (two - one) / 2); 151 | } 152 | else { 153 | return two + (long (one - two) / 2); 154 | } 155 | } 156 | 157 | 158 | //Quick and dirty DST check for flash time. 159 | bool DSTCheck(time_t flash) { 160 | int DSTy = 8; 161 | int DSTx = 13 - ((((getYear(flash) - 2016) / 4) * 2) + ((getYear(flash) - 2016) - ((getYear(flash) - 2016) / 4))); 162 | if (DSTx >= 8) { 163 | DSTy = DSTx; 164 | } 165 | else if (DSTx == 1) { 166 | DSTy = (DSTx + 7); 167 | } 168 | else { 169 | DSTy = (((((8 - DSTx) / 7) + 1) * 7) + DSTx); 170 | } 171 | if (flash < getTime(getYear(flash), 11, (DSTy - 7), 2, 0, 0) || flash >= getTime(getYear(flash), 3, DSTy, 2, 0, 0)) { 172 | return true; 173 | } 174 | else { 175 | return false; 176 | } 177 | } 178 | 179 | //jenkball method to anchor target daylight hours to Origin hours, centered on Origin's solar noon. If Target = Origin, it does nothing. 180 | time_t getRiseSet(float tLat, float tLon, int tTZ, float oLat, float oLon, int oTZ, time_t startDTTar, time_t startDTOrig, int SPosition, int ODiff, String RiseOrSet, int adj) { 181 | 182 | Dusk2Dawn target(tLat, tLon, tTZ); 183 | Dusk2Dawn origin(oLat, oLon, oTZ); 184 | 185 | startDTTar = startDTTar + ((SPosition + adj) * 86400L); 186 | startDTOrig = startDTOrig + ((ODiff + adj) * 86400L); 187 | 188 | int targetSunrise = target.sunrise(getYear(startDTTar), getMonth(startDTTar), getDay(startDTTar), false); 189 | int targetSunset = target.sunset(getYear(startDTTar), getMonth(startDTTar), getDay(startDTTar), false); 190 | 191 | int originSunrise = origin.sunrise(getYear(startDTOrig), getMonth(startDTOrig), getDay(startDTOrig), false); 192 | int originSunset = origin.sunset(getYear(startDTOrig), getMonth(startDTOrig), getDay(startDTOrig), false); 193 | 194 | time_t LStartTar = getTime(getYear(startDTTar), getMonth(startDTTar), getDay(startDTTar), int((targetSunrise) / 60), int((targetSunrise) % 60), 0); 195 | time_t LEndTar = getTime(getYear(startDTTar), getMonth(startDTTar), getDay(startDTTar), int((targetSunset) / 60), int((targetSunset) % 60), 0); 196 | time_t LStartOrig = getTime(getYear(startDTOrig), getMonth(startDTOrig), getDay(startDTOrig), int((originSunrise) / 60), int((originSunrise) % 60), 0); 197 | time_t LEndOrig = getTime(getYear(startDTOrig), getMonth(startDTOrig), getDay(startDTOrig), int((originSunset) / 60), int((originSunset) % 60), 0); 198 | 199 | if (RiseOrSet == "Rise") { 200 | return (halfTime(LEndOrig, LStartOrig)) - (difftime(LEndTar, LStartTar) / 2); 201 | } 202 | else if (RiseOrSet == "Set") { 203 | return (halfTime(LEndOrig, LStartOrig)) + (difftime(LEndTar, LStartTar) / 2); 204 | } 205 | 206 | } 207 | 208 | //The giant, disgusting basic function of daily temperature and humidity cycles. Sunrise to TPeak is anchored to the previous night's baseN value at sunrise. 209 | //TPeak to sunset is bound to the cosTDP-derived daily maximum value. 210 | float baseD(float minlow, float maxlow, float minhigh, float maxhigh, time_t currentTime, time_t LStart, time_t LEnd, time_t StartDT, int SPositionx, long TPeak, int xmin, int xmax, float baseNMinus, int adj) { 211 | 212 | int SPosition = SPositionx; 213 | int currentTimex = DayofYear(getDay(currentTime), getMonth(currentTime), getYear(currentTime)); 214 | int xminx = xmin; 215 | time_t StartDTadj = StartDT + ((SPosition + adj) * 86400L); 216 | int TarTimex = DayofYear(getDay(StartDTadj), getMonth(StartDTadj), getYear(StartDTadj)); 217 | 218 | if (xmax > xmin) { 219 | if (TarTimex >= xmax && xmax < 354) { 220 | xminx += 365; 221 | } 222 | else if (xmax >= 354) { 223 | if (TarTimex >= xmax) { 224 | xminx += 365; 225 | } 226 | else if (TarTimex < xmin) { 227 | xminx += 0; 228 | } 229 | } 230 | } 231 | 232 | else if (xmin > xmax) { 233 | if (TarTimex >= xmin && xmin < 354) { 234 | xminx -= 365; 235 | } 236 | else if (xmin >= 354) { 237 | if (TarTimex >= xmin) { 238 | xminx += 0; 239 | } 240 | else if (TarTimex < xmax) { 241 | xminx -= 365; 242 | } 243 | } 244 | } 245 | 246 | int AdjDOY = DayofYear(getDay(StartDTadj), getMonth(StartDTadj), getYear(StartDTadj)); 247 | if (AdjDOY >= 365 && DayofYear(getDay(StartDT), getMonth(StartDT), getYear(StartDT)) <= 1) { 248 | if (getYear(StartDTadj) < getYear(StartDT)) { 249 | AdjDOY -= 365; 250 | } 251 | } 252 | 253 | float ymin = cosTDP(AdjDOY, minlow, maxlow, xminx, (getDOYRange(xmin, xmax, StartDTadj))); 254 | 255 | if (currentTime < (LStart + TPeak)) { 256 | ymin = baseNMinus; 257 | } 258 | 259 | float currentX = (((currentTime - LStart) / 3600) + (float((currentTime - LStart) % 3600) / 3600)); 260 | float ymax = cosTDP(AdjDOY, minhigh, maxhigh, xminx, (getDOYRange(xmin, xmax, StartDTadj))); 261 | float cycleX = ((((LStart + TPeak) - LStart) / 3600) + (float(((LStart + TPeak) - LStart) % 3600) / 3600)) * 2; 262 | 263 | return polyTDP(currentX, ymin, ymax, 0, cycleX); 264 | //return paraTDP(currentX, ymin, ymax, 0, cycleX); 265 | } 266 | 267 | //Calculations for nighttime temperature and humidity. Starting point derived regressively from BaseD values at sunset. 268 | float baseN(float minlow, float maxlow, float minhigh, float maxhigh, time_t currentTime, time_t LStart, time_t LEnd, time_t StartDT, int SPosition, long TPeak, int xmin, int xmax, time_t LStartN, int adj) { 269 | int SPosS, SPosE = 0; 270 | time_t StartDTadj = StartDT + ((SPosition + adj)* 86400L); 271 | int AdjDOY = DayofYear(getDay(StartDTadj), getMonth(StartDTadj), getYear(StartDTadj)); 272 | if (AdjDOY >= 365 && DayofYear(getDay(StartDT), getMonth(StartDT), getYear(StartDT)) <= 1) { 273 | if (getYear(StartDTadj) < getYear(StartDT)) { 274 | AdjDOY -= 365; 275 | } 276 | } 277 | float x = ((currentTime - LEnd) / 3600) + (float((currentTime - LEnd) % 3600) / 3600); 278 | int currentTimex = DayofYear(getDay(currentTime), getMonth(currentTime), getYear(currentTime)); 279 | int xminx = xmin; 280 | int TarTimex = DayofYear(getDay(StartDTadj), getMonth(StartDTadj), getYear(StartDTadj)); 281 | 282 | 283 | 284 | if (xmax > xmin) { 285 | if (TarTimex >= xmax && xmax < 354) { 286 | xminx += 365; 287 | } 288 | else if (xmax >= 354) { 289 | if (TarTimex >= xmax) { 290 | xminx += 365; 291 | } 292 | else if (TarTimex < xmin) { 293 | xminx += 0; 294 | } 295 | } 296 | } 297 | 298 | else if (xmin > xmax) { 299 | if (TarTimex >= xmin && xmin < 354) { 300 | xminx -= 365; 301 | } 302 | else if (xmin >= 354) { 303 | if (TarTimex >= xmin) { 304 | xminx += 0; 305 | } 306 | else if (TarTimex < xmax) { 307 | xminx -= 365; 308 | } 309 | } 310 | } 311 | 312 | if ((getDay(LStart) != getDay(LEnd))) { 313 | if (getDay(currentTime) == getDay(LEnd)) { 314 | SPosS += 1; 315 | } 316 | else if (getDay(currentTime) == getDay(LStart)) { 317 | SPosE-=1; 318 | } 319 | 320 | float LEndVal = baseD(minlow, maxlow, minhigh, maxhigh, LEnd, LStartN, LEnd, StartDT, SPosition, TPeak, xminx, xmax, 0, (SPosE + adj)); 321 | float LStartVal = cosTDP(AdjDOY, minlow, maxlow, xminx, (getDOYRange(xmin, xmax, StartDTadj))); 322 | 323 | //the .8 constant wasn't really derived from any legit calculations, it was just the product of a lot of fiddling to see what came closest. Tinker with it as you see fit. 324 | return ((LEndVal - LStartVal) * (pow(.8, x))) + LStartVal; 325 | } 326 | } 327 | 328 | 329 | //Convert Relative Humidity to Dewpoint. Included because why not. Requires RH and Temperature at a given point in time. 330 | //If you cant's get dewpoint in your weather data, but you have RH and Temperature at the low and high points of the hottest and coldest days, it should serve just fine. 331 | float RHtoDP(float Temp, float RH, String CorF) { 332 | if (CorF == "F") { 333 | Temp -= 32; 334 | Temp /= 1.8; 335 | return ((243.04 * (log(RH / 100) + ((17.625 * Temp) / (243.04 + Temp))) / (17.625 - log(RH / 100) - ((17.625 * Temp)/(243.04 + Temp)))) * (1.8)) + 32; 336 | } 337 | else if (CorF == "C") { 338 | return 243.04 * (log(RH / 100) + ((17.625 * Temp) / (243.04 + Temp))) / (17.625 - log(RH / 100) - ((17.625 * Temp)/(243.04 + Temp))); 339 | } 340 | } 341 | 342 | //returns the number of seconds past dawn that thermal peak occurs. Necessary for getting cycle length in baseD. 343 | long getTPeak(time_t LStart, time_t LEnd, int TPeakDelay) { 344 | return(difftime((halfTime(LEnd, LStart) + (TPeakDelay * 60)), LStart)); 345 | } 346 | 347 | //returns number of days between hottest and coldest day of the year for whenever currentTime is relative to those days. 348 | //Creates a reasonable facsimile of seasonal lag without diving into complex calculations of solar irradiance. 349 | int getDOYRange(int xmin, int xmax, time_t currentTimex) { 350 | int currentTime = (DayofYear(getDay(currentTimex), getMonth(currentTimex), getYear(currentTimex))); 351 | if (xmin < xmax) { 352 | if ((xmin <= currentTime) && (currentTime < xmax)) { 353 | return (xmax - xmin) * 2; 354 | } 355 | else { 356 | return ((xmin - xmax) + 365) * 2; 357 | } 358 | } 359 | else if (xmax < xmin) { 360 | if ((xmax <= currentTime) && (currentTime < xmin)) { 361 | return (xmin - xmax) * 2; 362 | } 363 | else { 364 | return ((xmax - xmin) + 365) * 2; 365 | } 366 | } 367 | } 368 | 369 | time_t calcSun(time_t currentTime, float tLat, float tLon, int tTZ, float oLat, float oLon, int oTZ, time_t startDTTar, time_t startDTOrig, int SLength, String RiseOrSet) { 370 | 371 | //Total time from season start and position within the seasonal loop, respectively// 372 | int ODiff = (difftime(currentTime, startDTOrig) / 86400L); 373 | int SPosition = ODiff % SLength; 374 | 375 | //sets adjusted sunrise and sunset values based on Target and Origin astronomical calculations 376 | time_t LStart = getRiseSet(tLat, tLon, tTZ, oLat, oLon, oTZ, startDTTar, startDTOrig, SPosition, ODiff, "Rise", 0); 377 | time_t LEnd = getRiseSet(tLat, tLon, tTZ, oLat, oLon, oTZ, startDTTar, startDTOrig, SPosition, ODiff, "Set", 0); 378 | 379 | 380 | /////////////////Contextually adjusts sunrise and sunset and regressive values, sets TPeak accordingly///////////////// 381 | if (LStart > LEnd) { 382 | if (currentTime >= LStart) { 383 | LEnd = getRiseSet(tLat, tLon, tTZ, oLat, oLon, oTZ, startDTTar, startDTOrig, SPosition, ODiff, "Set", 1); 384 | } 385 | else if (currentTime < LEnd) { 386 | LStart = getRiseSet(tLat, tLon, tTZ, oLat, oLon, oTZ, startDTTar, startDTOrig, SPosition, ODiff, "Rise", -1); 387 | } 388 | } 389 | else if (LEnd > LStart) { 390 | if (currentTime < LStart) { 391 | LEnd = getRiseSet(tLat, tLon, tTZ, oLat, oLon, oTZ, startDTTar, startDTOrig, SPosition, ODiff, "Set", -1); 392 | } 393 | else if (currentTime >= LEnd) { 394 | LStart = getRiseSet(tLat, tLon, tTZ, oLat, oLon, oTZ, startDTTar, startDTOrig, SPosition, ODiff, "Rise", 1); 395 | 396 | } 397 | } 398 | 399 | if (RiseOrSet == "Rise") { 400 | return LStart; 401 | } 402 | else if (RiseOrSet == "Set") { 403 | return LEnd; 404 | } 405 | } 406 | 407 | time_t getSun(time_t currentTime, struct SetTarget_Origin Tar, struct SetTarget_Origin Orig, String RiseOrSet) { 408 | return calcSun(currentTime, Tar.Lat, Tar.Lon, Tar.TZ, Orig.Lat, Orig.Lon, Orig.TZ, Tar.StartDT, Orig.StartDT, Tar.SLength, RiseOrSet); 409 | } 410 | 411 | float calcTDP (time_t currentTime, float tLat, float tLon, int tTZ, float oLat, float oLon, int oTZ, time_t StartDT, time_t StartDTO, int SLength, time_t xminL, time_t xmaxL, float TDPminlow, float TDPminhigh, float TDPmaxlow, float TDPmaxhigh, int TPeakDelay) { 412 | 413 | int xmin = DayofYear(getDay(xminL), getMonth(xminL), getYear(xminL)); 414 | int xmax = DayofYear(getDay(xmaxL), getMonth(xmaxL), getYear(xmaxL)); 415 | 416 | //Total time from season start and position within the seasonal loop, respectively// 417 | int ODiff = (difftime(currentTime, StartDTO) / 86400L); 418 | int SPosition = ODiff % SLength; 419 | 420 | //sets adjusted sunrise and sunset values based on Target and Origin astronomical calculations 421 | time_t LStart = getRiseSet(tLat, tLon, tTZ, oLat, oLon, oTZ, StartDT, StartDTO, SPosition, ODiff, "Rise", 0); 422 | time_t LEnd = getRiseSet(tLat, tLon, tTZ, oLat, oLon, oTZ, StartDT, StartDTO, SPosition, ODiff, "Set", 0); 423 | //Sets regressive sunrise and sunset values 424 | time_t LStartN = getRiseSet(tLat, tLon, tTZ, oLat, oLon, oTZ, StartDT, StartDTO, SPosition, ODiff, "Rise", -1); 425 | time_t LEndN = getRiseSet(tLat, tLon, tTZ, oLat, oLon, oTZ, StartDT, StartDTO, SPosition, ODiff, "Set", -1); 426 | 427 | //Defines the thermal peak of a day's cycle based on start and end times; "TPeakDelay" is set to 150 minutes after solar noon by default on the front end in getTDP function; feel free to play with that based on local conditions 428 | long TPeak = getTPeak(LStart, LEnd, TPeakDelay); 429 | 430 | 431 | 432 | /////////////////Contextually adjusts sunrise and sunset and regressive values, sets TPeak accordingly///////////////// 433 | if (LStart > LEnd) { 434 | if (currentTime >= LStart) { 435 | LEndN = LEnd; 436 | LEnd = getRiseSet(tLat, tLon, tTZ, oLat, oLon, oTZ, StartDT, StartDTO, SPosition, ODiff, "Set", 1); 437 | TPeak = getTPeak(LStart, LEnd, TPeakDelay); 438 | } 439 | else if (currentTime < LEnd) { 440 | LStart = getRiseSet(tLat, tLon, tTZ, oLat, oLon, oTZ, StartDT, StartDTO, SPosition, ODiff, "Rise", -1); 441 | LEndN = getRiseSet(tLat, tLon, tTZ, oLat, oLon, oTZ, StartDT, StartDTO, SPosition, ODiff, "Set", -1); 442 | TPeak = getTPeak(LStart, LEnd, TPeakDelay); 443 | } 444 | } 445 | else if (LEnd > LStart) { 446 | if (currentTime < LStart) { 447 | LEnd = getRiseSet(tLat, tLon, tTZ, oLat, oLon, oTZ, StartDT, StartDTO, SPosition, ODiff, "Set", -1); 448 | TPeak = getTPeak((getRiseSet(tLat, tLon, tTZ, oLat, oLon, oTZ, StartDT, StartDTO, SPosition, ODiff, "Rise", -1)), LEnd, TPeakDelay); 449 | } 450 | else if (currentTime >= LEnd) { 451 | LStartN = LStart; 452 | LStart = getRiseSet(tLat, tLon, tTZ, oLat, oLon, oTZ, StartDT, StartDTO, SPosition, ODiff, "Rise", 1); 453 | LEndN = LEnd; 454 | TPeak = getTPeak((getRiseSet(tLat, tLon, tTZ, oLat, oLon, oTZ, StartDT, StartDTO, SPosition, ODiff, "Rise", 0)), LEnd, TPeakDelay); 455 | } 456 | } 457 | 458 | ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 459 | 460 | 461 | //Daytime logic. If this is true, the sun is up. 462 | if (LStart <= currentTime && currentTime < LEnd) { 463 | int SPositionx = SPosition - 1; 464 | 465 | if ((getDay(LStart) != getDay(LEnd)) && (getDay(currentTime) == getDay(LEnd))) { 466 | SPosition = SPositionx; 467 | } 468 | //Gets regressive projected values for temperature and dewpoint using baseN at dawn. Necessary for everything up to TPeak. 469 | float baseNMinusTDP = baseN(TDPminlow, TDPmaxlow, TDPminhigh, TDPmaxhigh, LStart, LStart, LEndN, StartDT, SPosition, TPeak, xmin, xmax, LStartN, -1); 470 | return baseD(TDPminlow, TDPmaxlow, TDPminhigh, TDPmaxhigh, currentTime, LStart, LEnd, StartDT, SPosition, TPeak, xmin, xmax, baseNMinusTDP, 0); 471 | } 472 | 473 | //nighttime logic. If this is true, the sun isn't up. No defining regressive values necessary here; built into baseN. 474 | else { 475 | return baseN(TDPminlow, TDPmaxlow, TDPminhigh, TDPmaxhigh, currentTime, LStart, LEnd, StartDT, SPosition, TPeak, xmin, xmax, LStartN, 0); 476 | } 477 | } 478 | 479 | float getTDP (time_t currentTime, struct SetTarget_Origin Tar, struct SetTarget_Origin Orig, struct SetRangeTDP Range, int TPeakDelay) { 480 | return calcTDP(currentTime, Tar.Lat, Tar.Lon, Tar.TZ, Orig.Lat, Orig.Lon, Orig.TZ, Tar.StartDT, Orig.StartDT, Tar.SLength, Range.xminL, Range.xmaxL, Range.minlow, Range.minhigh, Range.maxlow, Range.maxhigh, TPeakDelay); 481 | } 482 | 483 | //Convert Dewpoint and Temperature to Relative Humidity. Necessary for sensor readings. Requires Temperature, Dewpoint, and system of measurement. 484 | float getRH(float Tempx, float DPx, String CorF) { 485 | float Temp = Tempx; 486 | float DP = DPx; 487 | if (CorF == "F") { 488 | Temp = (Tempx - 32) / 1.8; 489 | DP = (DPx - 32) / 1.8; 490 | } 491 | return 100 * (exp((17.625 * DP)/(243.04 + DP)) / exp((17.625 * Temp)/(243.04 + Temp))); 492 | } 493 | --------------------------------------------------------------------------------