Here is the current code. I will keep this post updated with my latest version.
Code: Select all
#include <Salinity.h>
#include <ReefAngel_Features.h>
#include <Globals.h>
#include <RA_TS.h>
#include <RA_TouchLCD.h>
#include <RA_TFT.h>
#include <RA_TS.h>
#include <Font.h>
#include <RA_Wifi.h>
#include <RA_Wiznet5100.h>
#include <SD.h>
#include <SPI.h>
#include <Ethernet.h>
#include <EthernetDHCP.h>
#include <PubSubClient.h>
#include <Wire.h>
#include <OneWire.h>
#include <Time.h>
#include <DS1307RTC.h>
#include <InternalEEPROM.h>
#include <RA_ATO.h>
#include <LED.h>
#include <RA_TempSensor.h>
#include <Relay.h>
#include <RA_PWM.h>
#include <Timer.h>
#include <Memory.h>
#include <InternalEEPROM.h>
#include <RA_Colors.h>
#include <RA_CustomColors.h>
#include <RA_CustomLabels.h>
#include <RF.h>
#include <IO.h>
#include <ORP.h>
#include <AI.h>
#include <PH.h>
#include <WaterLevel.h>
#include <Humidity.h>
#include <PAR.h>
#include <DCPump.h>
#include <ReefAngel.h>
#include <SoftwareSerial.h>
#include <SunLocation.h>
#include <Tide.h>
#include <WiFiAlert.h>
#include <Moon.h>
////// Place global variable code below here
// Define Custom Memory Locations
#define Mem_B_MoonOffset 100
#define Mem_B_Vacation 101
#define Mem_B_AutoFeed 102
#define Mem_B_AutoFeedPress 103
#define Mem_B_AutoFeedRepeat 104
#define Mem_B_AutoFeedOffset 105
#define Mem_I_WCFillTime 106
#define Mem_I_Latitude 108
#define Mem_I_Longitude 110
#define Mem_B_AcclRiseOffset 112
#define Mem_B_AcclSetOffset 113
#define Mem_B_AcclDay 114
#define Mem_B_SwabbieRepeat 115
#define Mem_B_SwabbieTime 116
#define Mem_B_TideMin 117
#define Mem_B_TideMax 118
#define Mem_B_PumpOffset 119
#define Mem_B_FeedingRF 120
#define Mem_B_NightRF 121
#define Mem_B_NightSpeed 122
#define Mem_B_NightDuration 123
#define Mem_B_NTMSpeed 124
#define Mem_B_NTMDuration 125
#define Mem_B_NTMDelay 126
#define Mem_B_NTMTime 127
#define Mem_I_CalDP1Vol 128
#define Mem_I_CalDP1Time 130
#define Mem_I_DP1Volume 132
#define Mem_I_CalDP2Vol 134
#define Mem_I_CalDP2Time 136
#define Mem_I_DP2Volume 138
#define Mem_B_LogATO 140
#define Mem_B_LogPrevATO 141 // Free
#define Mem_B_AlkMaxDelta 142 // Free
#define Mem_B_TideMode 143
#define Mem_B_MaintGAC 144 // KRS
#define Mem_B_MaintGFO 145 // Scrubber
#define Mem_B_MaintCal 146
#define Mem_B_MaintAlk 147
#define Mem_B_MaintWC 148 // Reactor
#define Mem_B_MaintATO 149
#define Mem_B_MaintFeeding 150
#define Mem_B_MaintSkimmer 151
#define Mem_B_MaintSocks 152 // Klir
#define Mem_B_MaintWCVol 153 // Free
#define Mem_I_CalDP3Vol 154
#define Mem_I_CalDP3Time 156
#define Mem_I_DP3Volume 158
#define Mem_B_LightMode 160
#define Mem_B_LightOffset 161
#define Mem_I_RiseOffset 162
#define Mem_I_SetOffset 164
#define Mem_B_AcclActinicOffset 166
#define Mem_B_AcclDaylightOffset 167
#define Mem_B_RandomMode 168
#define Mem_B_GyreOffset 169
#define Mem_B_MoonMode 170
#define Mem_B_LightsOffPerc 171
#define Mem_B_FeedingSpeed 172
#define Mem_B_WCSpeed 173
#define Mem_B_AlkTarget 174
#define Mem_B_PowerOutageSpeed 175
#define Mem_B_VinegarWeek 176
#define Mem_B_MaintVinegar 177 // Magnesium
#define Mem_B_EnableStorm 178
#define Mem_B_ForceRandomTide 179
#define Mem_I_AWCTime 180
#define Mem_I_AWCFrequency 182
#define Mem_I_AWCOffset 184
#define Mem_I_MixTime 186
#define Mem_I_MixFrequency 188
#define Mem_B_FlushTime 190
#define Mem_I_DP1Log 191
#define Mem_I_DP2Log 193
#define Mem_I_DP3Log 195
#define Mem_B_AlkAdjVol 197 // Defines the total daily volume we're allowed to add
#define Mem_B_AlkAdjDelta 198 // Defines the bounds of our adjustment (ie. +/- 20)
#define Mem_B_ResetMemory 199
void init_memory() {
// Initialize Custom Memory Locations
InternalMemory.write(Mem_B_MoonOffset,30);
InternalMemory.write(Mem_B_Vacation,false);
InternalMemory.write(Mem_B_AutoFeed,true);
InternalMemory.write(Mem_B_AutoFeedPress,2);
InternalMemory.write(Mem_B_AutoFeedRepeat,4);
InternalMemory.write(Mem_B_AutoFeedOffset,3);
InternalMemory.write_int(Mem_I_WCFillTime,270);
InternalMemory.write_int(Mem_I_Latitude,21);
InternalMemory.write_int(Mem_I_Longitude,-73);
InternalMemory.write(Mem_B_AcclRiseOffset,4);
InternalMemory.write(Mem_B_AcclSetOffset,2);
InternalMemory.write(Mem_B_AcclDay,0);
InternalMemory.write(Mem_B_SwabbieRepeat,6);
InternalMemory.write(Mem_B_SwabbieTime,1);
InternalMemory.write(Mem_B_TideMin,10);
InternalMemory.write(Mem_B_TideMax,20);
InternalMemory.write(Mem_B_PumpOffset,80);
InternalMemory.write(Mem_B_FeedingRF,false);
InternalMemory.write(Mem_B_NightRF,false);
InternalMemory.write(Mem_B_NightSpeed,35);
InternalMemory.write(Mem_B_NightDuration,16);
InternalMemory.write(Mem_B_NTMSpeed,60);
InternalMemory.write(Mem_B_NTMDuration,5);
InternalMemory.write(Mem_B_NTMDelay,15);
InternalMemory.write(Mem_B_NTMTime,150);
InternalMemory.write_int(Mem_I_CalDP1Vol,10);
InternalMemory.write_int(Mem_I_CalDP1Time,470);
InternalMemory.write_int(Mem_I_DP1Volume,42);
InternalMemory.write_int(Mem_I_CalDP2Vol,10);
InternalMemory.write_int(Mem_I_CalDP2Time,490);
InternalMemory.write_int(Mem_I_DP2Volume,42);
InternalMemory.write_int(Mem_I_CalDP3Vol,10);
InternalMemory.write_int(Mem_I_CalDP3Time,480);
InternalMemory.write_int(Mem_I_DP3Volume,45);
InternalMemory.write(Mem_B_AlkMaxDelta,10);
InternalMemory.write(Mem_B_TideMode,0);
InternalMemory.write(Mem_B_LightOffset,20);
InternalMemory.write(Mem_B_LightMode,1);
InternalMemory.write_int(Mem_I_RiseOffset,-1);
InternalMemory.write_int(Mem_I_SetOffset,-1);
InternalMemory.write(Mem_B_AcclActinicOffset,100);
InternalMemory.write(Mem_B_AcclDaylightOffset,100);
InternalMemory.write(Mem_B_RandomMode,false);
InternalMemory.write(Mem_B_GyreOffset,10);
InternalMemory.write(Mem_B_MoonMode,1);
InternalMemory.write(Mem_B_LightsOffPerc,0);
InternalMemory.write(Mem_B_FeedingSpeed,3);
InternalMemory.write(Mem_B_WCSpeed,25);
InternalMemory.write(Mem_B_AlkTarget,145);
InternalMemory.write(Mem_B_PowerOutageSpeed,15);
InternalMemory.write(Mem_B_EnableStorm,true);
InternalMemory.write(Mem_B_ForceRandomTide,false);
InternalMemory.write_int(Mem_I_AWCTime,60);
InternalMemory.write_int(Mem_I_AWCFrequency,120);
InternalMemory.write_int(Mem_I_AWCOffset,0);
InternalMemory.write_int(Mem_I_MixTime,60);
InternalMemory.write_int(Mem_I_MixFrequency,120);
InternalMemory.write(Mem_B_FlushTime,30);
InternalMemory.write(Mem_B_AlkAdjVol,0);
// InternalMemory.write(Mem_B_LogATO,0);
// InternalMemory.write(Mem_B_LogPrevATO,0);
// InternalMemory.write(Mem_B_MaintGAC,0);
// InternalMemory.write(Mem_B_MaintGFO,0);
// InternalMemory.write(Mem_B_MaintCal,0);
// InternalMemory.write(Mem_B_MaintAlk,0);
// InternalMemory.write(Mem_B_MaintWC,0);
// InternalMemory.write(Mem_B_MaintATO,0);
// InternalMemory.write(Mem_B_MaintFeeding,0);
// InternalMemory.write(Mem_B_MaintSkimmer,0);
// InternalMemory.write(Mem_B_MaintSocks,0);
// InternalMemory.write(Mem_B_MaintVinegar,0);
InternalMemory.write(Mem_B_ResetMemory,false);
}
// Define Portal Variables
#define Var_DPump1 0
#define Var_DPump2 1
#define Var_DPump3 2
#define Var_Baseline 3
#define Var_Tide 4
#define Var_TideMode 5
#define Var_LogATO 6
#define Var_AdjustAlk 7
// Define Relay Ports by Name
#define DPump1 Box1_Port1 // Return
#define Skimmer Box1_Port2
#define DPump2 Box1_Port3 // Fan
#define Heater Box1_Port4
#define DPump3 Box1_Port5 // Vortech
#define Unused Box1_Port6 // DPump3
#define Fan Box1_Port7 // DPump2
#define Vortechs Box1_Port8 // DPump1
#define Swabbie Box2_Port1
#define Return Box2_Port2 // Unused
#define Feeder Box2_Port3
#define ATOSolenoid Box2_Port4
#define Refugium Box2_Port5
#define BlueLED Box2_Port6
#define Reactor Box2_Port7
#define WhiteLED Box2_Port8
// Box 3 and 4 are virtual ports
#define RelayPS Box5_Port1
#define MixPump Box5_Port2
#define ROSolenoid Box5_Port7
#define AWCPump Box5_Port8
#define VO_RefillATO Box3_Port1
#define VO_EnableATO Box3_Port2
#define VO_StartFill Box3_Port3
#define VO_Vacation Box3_Port4
#define VO_AutoFeed Box3_Port5
#define VO_SumpLights Box3_Port6
#define VO_Calibrate Box3_Port7
#define VO_LockPorts Box3_Port8
// Box4 Defined later
// Custom classes
SunLocation sun;
Tide tide;
// Vortech Variables
byte vtMode, vtSpeed, vtDuration;
// For Cloud and preset code
int DaylightPWMValue=0;
int ActinicPWMValue=0;
int Daylight2PWMValue=0;
int Actinic2PWMValue=0;
int DaylightPWMValue0=0; // For cloud code, channel 0
int DaylightPWMValue2=0; // For cloud code, chennel 2
int ActinicPWMValue1=0; // For cloud code, channel 0
int ActinicPWMValue3=0; // For cloud code, chennel 2
int DaylightPWMValue4=0;
int ActinicPWMValue5=0;
////// Place global variable code above here
void setup()
{
// This must be the first line
ReefAngel.Init(); //Initialize controller
ReefAngel.Star();
// Ports toggled in Feeding Mode
ReefAngel.FeedingModePortsE[0] = Port2Bit;
ReefAngel.FeedingModePortsE[1] = Port2Bit | Port5Bit | Port7Bit;
// Ports toggled in Water Change Mode
// ReefAngel.WaterChangePorts = Port2Bit | Port8Bit;
ReefAngel.WaterChangePortsE[0] = Port2Bit;
ReefAngel.WaterChangePortsE[1] = Port4Bit | Port7Bit;
// Ports turned off when Overheat temperature exceeded
ReefAngel.OverheatShutoffPortsE[0] = Port4Bit;
ReefAngel.OverheatShutoffPortsE[0] = Port4Bit | Port6Bit | Port8Bit;
// Ports toggled when Lights On / Off menu entry selected
ReefAngel.LightsOnPortsE[2] = Port6Bit;
// Use T1 probe as temperature and overheat functions
ReefAngel.TempProbe = T1_PROBE;
ReefAngel.OverheatProbe = T2_PROBE;
// Set the Overheat temperature setting
InternalMemory.OverheatTemp_write( 830 );
////// Place additional initialization code below here
ReefAngel.AddPHExpansion();
ReefAngel.AddExtraTempProbes(); // Additional Temp Probes on Hub
ReefAngel.AddMultiChannelWaterLevelExpansion(); // MultiChannelWaterLevelExpansion
ReefAngel.AddSalinityExpansion(); // Salinity Expansion Module
ReefAngel.Salinity.SetCompensation(0);
// Ports that default on
ReefAngel.Relay.On(Return);
ReefAngel.Relay.On(Vortechs);
ReefAngel.Relay.On(VO_LockPorts);
// Ports that default off
ReefAngel.Relay.Off(DPump1);
ReefAngel.Relay.Off(DPump2);
ReefAngel.Relay.Off(DPump3);
ReefAngel.Relay.Off(Heater);
ReefAngel.Relay.Off(ATOSolenoid);
ReefAngel.Relay.Off(Swabbie);
ReefAngel.Relay.Off(Feeder);
////// Place additional initialization code below here
ReefAngel.RF.UseMemory=false;
randomSeed(now()/SECS_PER_DAY);
if (InternalMemory.read(Mem_B_ResetMemory))
init_memory();
////// Place additional initialization code above here
}
void loop()
{
ReefAngel.Relay.On(Reactor);
ReefAngel.StandardHeater(Heater);
ReefAngel.StandardFan(Fan);
// ReefAngel.MoonLights(Refugium);
ReefAngel.Relay.On(Refugium);
DelayedOnModes(Skimmer); // DelayedOn after mode change only
////// Place your custom code below here
// Lighting and Flow
SetSun(); // Setup Sun rise/set lighting
AcclimateLED(); // Apply acclimation dimming
SetMoon(); // Setup Moon rise/set lighting
FillInMoon(); // Fill in 5% to 0% gap in main LEDs
LEDPresets();
CheckCloud(); // Check for cloud and lightning.
UpdateLED();
SetTide(); // Set High/Low tide properties
SetRF(); // Set Vortech modes
// Automation
CheckATO(); // Calculate ATO reservoir evaporation
RefillATO(); // Automatic ATO reservoir refill
Vacation(); // Automation while on Vacation
RunFeeder(); // Automatic Fish feeder
RunSwabbie(); // Skimmer neck cleaner
RunDosingPumps(); // Dose by volume or time
CalibrateDPumps(); // Calibrate Dosing pumps
AutoWaterChange(); // Automated water change
adjustAlk(); // Adjust Alkalinity to desired PPM (thanks AlanM)
Reminders(); // Increment maintenance counters
DailyReport(); // Needs portal to initialize first
LogFeedings(); // Track a relay
LogDosingPumps(); // Keep track of dosing
RunMixingStation(); // Run the Mixing Station
SumpLights(); // Turn off refugium lights
LockPorts(); // Clear overrides for critical ports
CheckPower(); // Monitor for power outages
CheckSwitches(); // Monitor for float switches
////// Place your custom code above here
ReefAngel.Network.Cloud();
// This should always be the last line
ReefAngel.ShowTouchInterface();
}
void CheckPower() {
static time_t powerOutage=false;
static WiFiAlert powerAlert;
// Power Outage - turn off everything
if (!ReefAngel.Relay.IsRelayPresent(EXP1_RELAY) && !powerOutage) // Expansion Relay NOT present
{
byte rfPowerOutageSpeed=InternalMemory.read(Mem_B_PowerOutageSpeed);
powerOutage=now();
if (now()-powerOutage > 300) {
ReefAngel.Relay.Set(Return, now()%3600<900);
}
ReefAngel.Relay.Off(Heater);
ReefAngel.Relay.Off(Skimmer);
ReefAngel.RF.SetMode(Constant,rfPowerOutageSpeed,0);
if (now()-powerOutage > 10) { // Wait 10 seconds before sending to avoid false alarms
powerAlert.Send("Power+outage!");
}
}
// Power Restored - Turn things back on
if (powerOutage && ReefAngel.Relay.IsRelayPresent(EXP1_RELAY))
{
powerOutage=0;
LastStart=now();
ReefAngel.RF.SetMode(vtMode,vtSpeed,vtDuration);
powerAlert.Send("Power+restored.",true);
}
if(powerAlert.IsActive()) powerAlert.Send();
}
void CheckSwitches() {
static boolean returnOverride=true;
static WiFiAlert switchAlert, skimmerAlert, atoAlert;
//skimmerAlert.SetDelay(SECS_PER_HOUR);
skimmerAlert.SetDelay(SECS_PER_DAY);
atoAlert.SetDelay(SECS_PER_HOUR);
if (ReefAngel.DisplayedMenu==FEEDING_MODE || ReefAngel.DisplayedMenu==WATERCHANGE_MODE)
returnOverride=true;
// Turn off return if sump overflowing or out of water in return
ReefAngel.ReverseATOHigh();
ReefAngel.ReverseATOLow();
if (!ReefAngel.HighATO.IsActive() || !ReefAngel.LowATO.IsActive()) {
if (!returnOverride) {
switchAlert.Send("Sump+level+alarm!+Return+pump+disabled.");
ReefAngel.Relay.Override(Return,0);
returnOverride=true;
}
} else {
returnOverride=false;
}
// Turn off Skimmer if waste collector is full.
if (!ReefAngel.AlarmInput.IsActive()) { // switch is on by default
skimmerAlert.Send("Skimmate+container+full!+Skimmer+disabled.");
ReefAngel.Relay.Override(Skimmer,0);
if (InternalMemory.read(Mem_B_MaintSkimmer) > 0) // Reset last Skimmer counter
InternalMemory.write(Mem_B_MaintSkimmer,0);
}
// Turn off Skimmer if Return pump has been shutoff.
if (!ReefAngel.Relay.Status(Return)) {
ReefAngel.Relay.Override(Skimmer,0);
}
// Alert if ATO timeput flag
if (bitRead(ReefAngel.AlertFlags,ATOTimeOutFlag)) {
atoAlert.Send("ATO+timeout!+ATO+disabled.");
}
}
void SetSun() {
// Start acclimation routine
int acclRiseOffset=InternalMemory.read(Mem_B_AcclRiseOffset)*60;
int acclSetOffset=InternalMemory.read(Mem_B_AcclSetOffset)*60;
byte acclDay=InternalMemory.read(Mem_B_AcclDay);
// See if we are acclimating corals and decrease the countdown each day
static boolean acclCounterReady=false;
if (now()%SECS_PER_DAY!=0) acclCounterReady=true;
if (now()%SECS_PER_DAY==0 && acclCounterReady && acclDay>0) {
acclDay--;
acclCounterReady=false;
InternalMemory.write(Mem_B_AcclDay,acclDay);
}
// End acclimation
// Add some customizable offsets
sun.Init(InternalMemory.read_int(Mem_I_Latitude), InternalMemory.read_int(Mem_I_Longitude));
int riseOffset=InternalMemory.read_int(Mem_I_RiseOffset);
int setOffset=InternalMemory.read_int(Mem_I_SetOffset);
sun.SetOffset(riseOffset,(acclDay*acclRiseOffset),setOffset,(-acclDay*acclSetOffset)); // Bahamas
sun.CheckAndUpdate(); // Calculate today's Sunrise / Sunset
byte lightOffset=InternalMemory.read(Mem_B_LightOffset); // left right separation
byte actinicOffset=InternalMemory.ActinicOffset_read();
// Make sure light resets to zero at night.
for(int i=0;i<4;i++) { ReefAngel.PWM.SetChannel(i,0); }
switch(InternalMemory.read(Mem_B_LightMode)) {
case 0: {
// Daylights
ReefAngel.PWM.Channel0PWMSlope(0,lightOffset);
ReefAngel.PWM.Channel2PWMSlope(lightOffset,0);
// Actinics
ReefAngel.PWM.Channel1PWMSlope(actinicOffset,actinicOffset+lightOffset);
ReefAngel.PWM.Channel3PWMSlope(actinicOffset+lightOffset,actinicOffset);
break;
}
case 1: {
// Daylights
ReefAngel.PWM.Channel0PWMParabola(0,lightOffset);
ReefAngel.PWM.Channel2PWMParabola(lightOffset,0);
// Actinics
ReefAngel.PWM.Channel1PWMParabola(actinicOffset,actinicOffset+lightOffset);
ReefAngel.PWM.Channel3PWMParabola(actinicOffset+lightOffset,actinicOffset);
break;
}
case 2: {
// Daylights
ReefAngel.PWM.Channel0PWMSmoothRamp(0,lightOffset);
ReefAngel.PWM.Channel2PWMSmoothRamp(lightOffset,0);
// Actinics
ReefAngel.PWM.Channel1PWMSmoothRamp(actinicOffset,actinicOffset+lightOffset);
ReefAngel.PWM.Channel3PWMSmoothRamp(actinicOffset+lightOffset,actinicOffset);
break;
}
case 3: {
// Daylights
ReefAngel.PWM.Channel0PWMSigmoid(0,lightOffset);
ReefAngel.PWM.Channel2PWMSigmoid(lightOffset,0);
// Actinics
ReefAngel.PWM.Channel1PWMSigmoid(actinicOffset,actinicOffset+lightOffset);
ReefAngel.PWM.Channel3PWMSigmoid(actinicOffset+lightOffset,actinicOffset);
break;
}
}
}
void SetMoon() {
byte offset=InternalMemory.read(Mem_B_MoonOffset);
byte startD=InternalMemory.read(Mem_B_PWMSlopeStart4);
byte endD=InternalMemory.read(Mem_B_PWMSlopeEnd4);
byte timeD=InternalMemory.read(Mem_B_PWMSlopeDuration4);
byte startA=InternalMemory.read(Mem_B_PWMSlopeStart5);
byte endA=InternalMemory.read(Mem_B_PWMSlopeEnd5);
byte timeA=InternalMemory.read(Mem_B_PWMSlopeDuration5);
time_t onTime=ScheduleTime(Moon.riseH, Moon.riseM,0);
time_t offTime=ScheduleTime(Moon.setH, Moon.setM,0);
time_t offsetOnTime=ScheduleTime(Moon.riseH, Moon.riseM,0)-(offset*60);
time_t offsetOffTime=ScheduleTime(Moon.setH, Moon.setM,0)-(offset*60);
byte actRiseH=(offsetOnTime%SECS_PER_DAY)/SECS_PER_HOUR;
byte actRiseM=((offsetOnTime%SECS_PER_DAY)%SECS_PER_HOUR)/60;
byte actSetH=(offsetOffTime%SECS_PER_DAY)/SECS_PER_HOUR;
byte actSetM=((offsetOffTime%SECS_PER_DAY)%SECS_PER_HOUR)/60;
static byte mp=MoonPhase();
if (mp!=MoonPhase()) {
InternalMemory.write(Mem_B_PWMSlopeEnd4,mp);
InternalMemory.write(Mem_B_PWMSlopeEnd5,mp);
mp=MoonPhase();
}
moon_init(InternalMemory.read_int(Mem_I_Latitude), InternalMemory.read_int(Mem_I_Longitude));
// Make sure light resets to zero at night.
ReefAngel.PWM.SetChannel(4,0);
ReefAngel.PWM.SetChannel(5,0);
switch(InternalMemory.read(Mem_B_MoonMode)) {
case 0: {
// Daylights
ReefAngel.PWM.SetChannelRaw(4,PWMSlopeHighRes(actRiseH,actRiseM,actSetH,actSetM,startD,endD,timeD,0));
ReefAngel.PWM.SetChannelRaw(5,PWMSlopeHighRes(Moon.riseH,Moon.riseM,Moon.setH,Moon.setM,startA,endA,timeA,0));
break;
}
case 1: {
ReefAngel.PWM.SetChannelRaw(4,PWMParabolaHighRes(actRiseH,actRiseM,actSetH,actSetM, startD,endD,0));
ReefAngel.PWM.SetChannelRaw(5,PWMParabolaHighRes(Moon.riseH,Moon.riseM,Moon.setH,Moon.setM, startA,endA,0));
break;
}
case 2: {
ReefAngel.PWM.SetChannelRaw(4,PWMSmoothRampHighRes(actRiseH,actRiseM,actSetH,actSetM,startD,endD,timeD,0));
ReefAngel.PWM.SetChannelRaw(5,PWMSmoothRampHighRes(Moon.riseH,Moon.riseM,Moon.setH,Moon.setM,startA,endA,timeA,0));
break;
}
case 3: {
ReefAngel.PWM.SetChannelRaw(4,PWMSigmoidHighRes(actRiseH,actRiseM,actSetH,actSetM,startD,endD,0));
ReefAngel.PWM.SetChannelRaw(5,PWMSigmoidHighRes(Moon.riseH,Moon.riseM,Moon.setH,Moon.setM,startA,endA,0));
break;
}
}
}
void FillInMoon() {
// Extend the sunrise/sunset to fill in gaps when fixtures shut off.
int actinicOffset=60*InternalMemory.ActinicOffset_read();
int lightOffset=60*InternalMemory.read(Mem_B_MoonOffset); // left right separation
int LightsOffVal=40.95*(1+InternalMemory.read(Mem_B_LightsOffPerc));
int moonVal1, moonVal2,channelVal1, channelVal2 = 0;
int offset=actinicOffset+lightOffset;
time_t onTime=ScheduleTime(InternalMemory.StdLightsOnHour_read(),InternalMemory.StdLightsOnMinute_read(),0)-offset;
int onTime1=NumMins(hour(onTime-lightOffset+300),minute(onTime-lightOffset+300));
int onTime2=NumMins(hour(onTime-lightOffset),minute(onTime-lightOffset));
time_t offTime=ScheduleTime(InternalMemory.StdLightsOffHour_read(),InternalMemory.StdLightsOffMinute_read(),0)+offset;
int offTime1=NumMins(hour(offTime+lightOffset+300),minute(offTime+lightOffset+300));
int offTime2=NumMins(hour(offTime+lightOffset),minute(offTime+lightOffset));
if(ReefAngel.PWM.GetChannelValueRaw(1)<=LightsOffVal) {
moonVal1=ReefAngel.PWM.GetChannelValueRaw(4); // Left Moon
channelVal1=PWMSlopeHighRes(onTime1/60,onTime1%60,offTime1/60,offTime1%60,0,100,lightOffset/60,0);
if (channelVal1>moonVal1) ReefAngel.PWM.SetChannelRaw(4,channelVal1);
}
if(ReefAngel.PWM.GetChannelValueRaw(3)<=LightsOffVal) {
moonVal2=ReefAngel.PWM.GetChannelValueRaw(5); // Right Moon
channelVal2=PWMSlopeHighRes(onTime2/60,onTime2%60,offTime2/60,offTime2%60,0,100,lightOffset/60,0);
if (channelVal2>moonVal2) ReefAngel.PWM.SetChannelRaw(5,channelVal2);
}
DaylightPWMValue4=ReefAngel.PWM.GetChannelValueRaw(4);
ActinicPWMValue5=ReefAngel.PWM.GetChannelValueRaw(5);
}
void AcclimateLED() {
byte acclDay=InternalMemory.read(Mem_B_AcclDay);
if (acclDay > 0) {
float acclActinicOffset=acclDay*(40.95*(((float)InternalMemory.read(Mem_B_AcclActinicOffset)/100)));
float acclDaylightOffset=acclDay*(40.95*((float)InternalMemory.read(Mem_B_AcclDaylightOffset)/100));
float endPerc;
endPerc=40.95*InternalMemory.PWMSlopeEnd1_read();
ReefAngel.PWM.SetChannelRaw(1,map(ReefAngel.PWM.GetChannelValueRaw(1),0,endPerc,0,endPerc-acclActinicOffset));
endPerc=40.95*InternalMemory.PWMSlopeEnd3_read();
ReefAngel.PWM.SetChannelRaw(3,map(ReefAngel.PWM.GetChannelValueRaw(3),0,endPerc,0,endPerc-acclActinicOffset));
endPerc=40.95*InternalMemory.PWMSlopeEnd0_read();
ReefAngel.PWM.SetChannelRaw(0,map(ReefAngel.PWM.GetChannelValueRaw(0),0,endPerc,0,endPerc-acclDaylightOffset));
endPerc=40.95*InternalMemory.PWMSlopeEnd2_read();
ReefAngel.PWM.SetChannelRaw(2,map(ReefAngel.PWM.GetChannelValueRaw(2),0,endPerc,0,endPerc-acclDaylightOffset));
}
}
#define LED_1to1 Box4_Port1
#define LED_4to1 Box4_Port2
#define LED_3to1 Box4_Port3
#define LED_2to1 Box4_Port4
#define LED_BLUE Box4_Port5
#define LED_WHITE Box4_Port6
#define LED_MOON Box4_Port7
#define LED_STORM Box4_Port8
void resetRelayBox(byte ID) {
// toggle all relays except for the one selected
for (int i=Box4_Port1;i<=Box4_Port4;i++) {
if (i!=ID) ReefAngel.Relay.Auto(i);
}
}
void LEDPresets() {
static byte lastPreset=0;
DaylightPWMValue0=ReefAngel.PWM.GetChannelValueRaw(0);
ActinicPWMValue1=ReefAngel.PWM.GetChannelValueRaw(1);
DaylightPWMValue2=ReefAngel.PWM.GetChannelValueRaw(2);
ActinicPWMValue3=ReefAngel.PWM.GetChannelValueRaw(3);
DaylightPWMValue4=ReefAngel.PWM.GetChannelValueRaw(4);
ActinicPWMValue5=ReefAngel.PWM.GetChannelValueRaw(5);
if (ReefAngel.Relay.isMaskOn(LED_1to1)) {
if (lastPreset!=1) resetRelayBox(LED_1to1);
DaylightPWMValue0=90*40.95;
ActinicPWMValue1=10*40.95;
DaylightPWMValue2=90*40.95;
ActinicPWMValue3=10*40.95;
lastPreset=1;
}
if (ReefAngel.Relay.isMaskOff(LED_1to1)) {
if (lastPreset!=2) resetRelayBox(LED_1to1);
DaylightPWMValue0=10*40.95;
ActinicPWMValue1=90*40.95;
DaylightPWMValue2=10*40.95;
ActinicPWMValue3=90*40.95;
lastPreset=2;
}
if (ReefAngel.Relay.isMaskOn(LED_2to1)) {
if (lastPreset!=3) resetRelayBox(LED_2to1);
DaylightPWMValue0=60*40.95;
ActinicPWMValue1=40*40.95;
DaylightPWMValue2=60*40.95;
ActinicPWMValue3=40*40.95;
lastPreset=3;
}
if (ReefAngel.Relay.isMaskOff(LED_2to1)) {
if (lastPreset!=4) resetRelayBox(LED_2to1);
DaylightPWMValue0=40*40.95;
ActinicPWMValue1=60*40.95;
DaylightPWMValue2=40*40.95;
ActinicPWMValue3=60*40.95;
lastPreset=4;
}
if (ReefAngel.Relay.isMaskOn(LED_3to1)) {
if (lastPreset!=5) resetRelayBox(LED_3to1);
DaylightPWMValue0=75*40.95;
ActinicPWMValue1=25*40.95;
DaylightPWMValue2=75*40.95;
ActinicPWMValue3=25*40.95;
lastPreset=5;
}
if (ReefAngel.Relay.isMaskOff(LED_3to1)) {
if (lastPreset!=6) resetRelayBox(LED_3to1);
DaylightPWMValue0=25*40.95;
ActinicPWMValue1=75*40.95;
DaylightPWMValue2=25*40.95;
ActinicPWMValue3=75*40.95;
lastPreset=6;
}
if (ReefAngel.Relay.isMaskOn(LED_4to1)) {
if (lastPreset!=7) resetRelayBox(LED_4to1);
DaylightPWMValue0=80*40.95;
ActinicPWMValue1=20*40.95;
DaylightPWMValue2=80*40.95;
ActinicPWMValue3=20*40.95;
lastPreset=7;
}
if (ReefAngel.Relay.isMaskOff(LED_4to1)) {
if (lastPreset!=8) resetRelayBox(LED_4to1);
DaylightPWMValue0=20*40.95;
ActinicPWMValue1=80*40.95;
DaylightPWMValue2=20*40.95;
ActinicPWMValue3=80*40.95;
lastPreset=8;
}
if (ReefAngel.Relay.isMaskOn(LED_BLUE)) {
if (lastPreset!=9) resetRelayBox(LED_BLUE);
DaylightPWMValue0=0;
ActinicPWMValue1=80*40.95;
DaylightPWMValue2=0;
ActinicPWMValue3=80*40.95;
lastPreset=9;
}
if (ReefAngel.Relay.isMaskOff(LED_BLUE)) {
if (lastPreset!=10) resetRelayBox(LED_BLUE);
ActinicPWMValue1=0;
ActinicPWMValue3=0;
lastPreset=10;
}
if (ReefAngel.Relay.isMaskOn(LED_WHITE)) {
if (lastPreset!=11) resetRelayBox(LED_WHITE);
DaylightPWMValue0=80*40.95;
ActinicPWMValue1=0;
DaylightPWMValue2=80*40.95;
ActinicPWMValue3=0;
lastPreset=11;
}
if (ReefAngel.Relay.isMaskOff(LED_WHITE)) {
if (lastPreset!=12) resetRelayBox(LED_WHITE);
DaylightPWMValue0=0;
DaylightPWMValue2=0;
lastPreset=12;
}
if (ReefAngel.Relay.isMaskOn(LED_MOON)) {
if (lastPreset!=13) resetRelayBox(LED_MOON);
DaylightPWMValue4=4095;
ActinicPWMValue5=4095;
lastPreset=13;
}
if (ReefAngel.Relay.isMaskOff(LED_MOON)) {
if (lastPreset!=14) resetRelayBox(LED_MOON);
DaylightPWMValue4=0;
ActinicPWMValue5=0;
lastPreset=14;
}
}
// Write updated values to the channels
void UpdateLED() {
ReefAngel.PWM.SetChannelRaw(0,DaylightPWMValue0);
ReefAngel.PWM.SetChannelRaw(1,ActinicPWMValue1);
ReefAngel.PWM.SetChannelRaw(2,DaylightPWMValue2);
ReefAngel.PWM.SetChannelRaw(3,ActinicPWMValue3);
ReefAngel.PWM.SetChannelRaw(4,DaylightPWMValue4);
ReefAngel.PWM.SetChannelRaw(5,ActinicPWMValue5);
ReefAngel.PWM.SetActinic(ActinicPWMValue);
ReefAngel.PWM.SetDaylight(DaylightPWMValue);
ReefAngel.PWM.SetActinic2(Actinic2PWMValue);
ReefAngel.PWM.SetDaylight2(Daylight2PWMValue);
byte LightsOffPerc=40.95*InternalMemory.read(Mem_B_LightsOffPerc);
(ReefAngel.PWM.GetChannelValueRaw(0)>=LightsOffPerc || ReefAngel.PWM.GetChannelValueRaw(2)>=LightsOffPerc)?
ReefAngel.Relay.On(WhiteLED) : ReefAngel.Relay.Off(WhiteLED);
(ReefAngel.PWM.GetChannelValueRaw(1)>=LightsOffPerc || ReefAngel.PWM.GetChannelValueRaw(3)>=LightsOffPerc)?
ReefAngel.Relay.On(BlueLED) : ReefAngel.Relay.Off(BlueLED);
}
void SetTide() {
byte nightSpeed=InternalMemory.read(Mem_B_NightSpeed);
byte tideMin=InternalMemory.read(Mem_B_TideMin);
byte tideMax=InternalMemory.read(Mem_B_TideMax);
// Set tide offsets
tide.SetOffset(tideMin, tideMax);
// Set tide speed. Slope in/out of Night Mode
tide.SetSpeed(PWMSlope(sun.GetRiseHour()-1,sun.GetRiseMinute(),
sun.GetSetHour(),sun.GetSetMinute(),nightSpeed+tideMin,vtSpeed,120,nightSpeed+tideMin));
// Show tide info on portal
ReefAngel.CustomVar[Var_Tide]=tide.CalcTide();
}
void SetRF() {
int ntmDelay=InternalMemory.read(Mem_B_NTMDelay)*60;
int ntmTime=InternalMemory.read(Mem_B_NTMTime)*60;
boolean nightRF=InternalMemory.read(Mem_B_NightRF);
boolean feedingRF=InternalMemory.read(Mem_B_FeedingRF);
static time_t t;
ReefAngel.RF.FeedingSpeed=InternalMemory.read(Mem_B_FeedingSpeed);
ReefAngel.RF.WaterChangeSpeed=InternalMemory.read(Mem_B_WCSpeed);
vtMode=InternalMemory.RFMode_read();
vtSpeed=InternalMemory.RFSpeed_read();
vtDuration=InternalMemory.RFDuration_read();
if ((now()-t > ntmDelay && now()-t < ntmTime+ntmDelay) && feedingRF) {
// Post feeding mode
vtMode=Smart_NTM;
vtSpeed=InternalMemory.read(Mem_B_NTMSpeed);
vtDuration=InternalMemory.read(Mem_B_NTMDuration);
} else if (!sun.IsDaytime() && nightRF) {
vtMode=Night;
vtSpeed=InternalMemory.read(Mem_B_NightSpeed);
vtDuration=InternalMemory.read(Mem_B_NightDuration);
} else {
if (vtMode!=Night && ReefAngel.RF.Mode==Night)
ReefAngel.RF.SetMode(Night_Stop,0,0);
}
if (ReefAngel.DisplayedMenu==FEEDING_MODE) {
t=now(); // Run post feeding mode when this counter stops
} else if (ReefAngel.DisplayedMenu==WATERCHANGE_MODE) {
// Not needed anymore.
// ReefAngel.RF.SetMode(Constant,25,0);
} else {
if ((vtMode==Smart_NTM) || (vtMode==ShortPulse)) vtDuration=InternalMemory.read(Mem_B_NTMDuration);
(vtMode==Custom) ? RFCustom() : ReefAngel.RF.SetMode(vtMode,vtSpeed,vtDuration);
}
}
// Define new modes
#define BHazard 15
#define RA_ReefCrest 16
#define RA_Lagoon 17
#define RA_TidalSwell 18
#define RA_Smart_NTM 19
#define RA_ShortPulse 20
#define RA_LongPulse 21
void RFCustom() {
static boolean changeMode;
byte rcSpeed, rcSpeedAS;
byte tideSpeed=tide.CalcTide();
byte tideMin=InternalMemory.read(Mem_B_TideMin);
byte tideMax=InternalMemory.read(Mem_B_TideMax);
byte tideMode=InternalMemory.read(Mem_B_TideMode);
float pumpOffset=(float) InternalMemory.read(Mem_B_PumpOffset)/100;
const byte RandomModes[]={ ReefCrest, TidalSwell, Smart_NTM, Lagoon, ShortPulse, LongPulse, BHazard, Else, Sine, Constant };
// if (now()%SECS_PER_DAY!=0 && InternalMemory.read(Mem_B_RandomMode)) changeMode=true;
// if (now()%SECS_PER_DAY==0 && changeMode) {
if (now()%(6*SECS_PER_HOUR)!=10 && InternalMemory.read(Mem_B_RandomMode)) changeMode=true;
if (now()%(6*SECS_PER_HOUR)==10 && changeMode) {
tideMode=random(100)%sizeof(RandomModes);
InternalMemory.write(Mem_B_TideMode,tideMode);
changeMode=false;
}
// Choose another random mode if triggered
if (InternalMemory.read(Mem_B_ForceRandomTide)) {
tideMode=random(100)%sizeof(RandomModes);
InternalMemory.write(Mem_B_TideMode,tideMode);
InternalMemory.write(Mem_B_ForceRandomTide,false);
}
ReefAngel.CustomVar[Var_TideMode]=tideMode+1;
switch (RandomModes[tideMode]) {
case ReefCrest: {
ReefAngel.RF.SetMode(ReefCrest,tideSpeed,vtDuration);
return;
break;
}
case Lagoon: {
ReefAngel.RF.SetMode(Lagoon,tideSpeed,vtDuration);
return;
break;
}
case TidalSwell: {
ReefAngel.RF.SetMode(TidalSwell,tideSpeed,vtDuration);
return;
break;
}
case Smart_NTM: {
ReefAngel.RF.SetMode(Smart_NTM,tideSpeed,vtDuration);
return;
break;
}
case ShortPulse: {
ReefAngel.RF.SetMode(ShortPulse,tideSpeed,vtDuration);
return;
break;
}
case LongPulse: {
ReefAngel.RF.SetMode(LongPulse,tideSpeed,vtDuration);
return;
break;
}
case RA_ReefCrest: {
rcSpeed=ReefCrestMode(tideSpeed,vtDuration*2,true);
rcSpeedAS=ReefCrestMode(tideSpeed,vtDuration*2,false);
break;
}
case RA_Lagoon: {
rcSpeed=ReefCrestMode(tideSpeed,vtDuration,true);
rcSpeedAS=ReefCrestMode(tideSpeed,vtDuration,false);
break;
}
case RA_TidalSwell: {
rcSpeed=TidalSwellMode(tideSpeed,true);
rcSpeedAS=TidalSwellMode(tideSpeed,false);
break;
}
case RA_Smart_NTM: {
rcSpeed=NutrientTransportMode(0,tideSpeed,vtDuration*50,true);
rcSpeedAS=NutrientTransportMode(0,tideSpeed,vtDuration*50,false);
break;
}
case RA_ShortPulse: {
rcSpeed=ShortPulseMode(0,tideSpeed,vtDuration*50,true);
rcSpeedAS=ShortPulseMode(0,tideSpeed,vtDuration*50,false);
break;
}
case RA_LongPulse: {
rcSpeed=LongPulseMode(0,tideSpeed,vtDuration,true);
rcSpeedAS=LongPulseMode(0,tideSpeed,vtDuration,false);
break;
}
case Else: {
rcSpeed=ElseMode(tideSpeed,vtDuration*2,true);
rcSpeedAS=ElseMode(tideSpeed,vtDuration*2,false);
break;
}
case BHazard: {
rcSpeed=millis()%1200>800?tideSpeed:0;
rcSpeedAS=millis()%1200<400?0:tideSpeed;
break;
}
case Sine: {
rcSpeed=SineMode(tideSpeed-tideMin,tideSpeed+tideMin,vtDuration*100,true);
rcSpeedAS=SineMode(tideSpeed-tideMin,tideSpeed+tideMin,vtDuration*100,false);
break;
}
default: {
rcSpeed=tideSpeed;
rcSpeedAS=tideSpeed;
pumpOffset=(float) InternalMemory.read(Mem_B_GyreOffset)/100;
}
}
ReefAngel.RF.SetMode(Custom,rcSpeedAS*pumpOffset,tide.isOutgoing());
ReefAngel.RF.SetMode(Custom,rcSpeed,tide.isIncoming());
}
// ATO Refill mode. Top off ATO reservoir until it's at 100%
void RefillATO() {
static boolean refillActive=false;
static WiFiAlert refillAlert;
byte level=ReefAngel.WaterLevel.GetLevel();
byte lowLevel=InternalMemory.WaterLevelLow_read();
byte highLevel=InternalMemory.WaterLevelHigh_read();
static byte prevLevel;
if (ReefAngel.Relay.isMaskOff(VO_RefillATO)) return;
level=ReefAngel.WaterLevel.GetLevel();
if (now()%SECS_PER_DAY-300==20*SECS_PER_HOUR && level<lowLevel) {
ReefAngel.Relay.Override(VO_RefillATO,1);
ReefAngel.Relay.Override(ROSolenoid,1);
}
if (ReefAngel.Relay.Status(VO_RefillATO)) {
highLevel=InternalMemory.WaterLevelHigh_read();
if (level<=highLevel) {
/* Since we've activated this mode once we've reached the lowLevel in memory,
we can essentially ignore the ATO Low Level moving forward or until it times out.
We only really care about the High Level at this point. It is as if we were using
a SingleATO function. This should also allows us to override the lowlevel when we
want to manually top off the reservoir. It also prevents a mis-read on the sensor
from causing the ATO to StopTopping() and wait again till it hits the lowlevel to
restart. */
ReefAngel.WaterLevelATO(ATOSolenoid,InternalMemory.ATOExtendedTimeout_read(),highLevel-1,highLevel);
if (!refillActive) {
prevLevel=level;
refillAlert.Send("ATO+Refill+is+starting.",true);
}
refillActive=true;
} else {
ReefAngel.Relay.Auto(VO_RefillATO);
ReefAngel.Relay.Off(ATOSolenoid);
refillAlert.Send("ATO+Refill+is+complete.",true);
}
} else {
if (refillActive) {
if (InternalMemory.read(Mem_B_MaintATO) > 0) // Reset last ATO counter
InternalMemory.write(Mem_B_MaintATO,0);
ReefAngel.Relay.Off(ATOSolenoid);
ReefAngel.Relay.Override(ROSolenoid,1);
refillActive=false;
}
}
if (refillAlert.IsActive()) refillAlert.Send();
// Something in our timing with the WLATO is causing a TimeOut.
if ((hour()==20 && minute()<=5) && ReefAngel.Relay.Status(VO_RefillATO)) {
// ReefAngel.ATOClear();
}
}
// Calculate evaportation rate
void CheckATO() {
byte numHours, index, level, delta;
float duration;
const int logHours=12;
static float rate, logLevel[logHours];
static boolean rateSaved, rateInit, refillActive;
numHours=(now()%(SECS_PER_HOUR*logHours))/SECS_PER_HOUR;
level=ReefAngel.WaterLevel.GetLevel();
// Initialize the array based on last rate
// Wait a little to make sure WL reading properly. Or we're refilling.
if (now()-LastStart>1 && !rateInit) {
rateInit=true;
index=numHours;
rate=InternalMemory.read(Mem_B_LogATO);
for (byte i=0;i<logHours;i++) {
logLevel[index]=level+(i*(rate/24.0));
//Serial.print(i);
//Serial.print(": ");
//Serial.print(index);
//Serial.print(": ");
//Serial.println(logLevel[index]);
index>0?index--:index=11;
}
}
if (!ReefAngel.Relay.Status(VO_RefillATO) && rateInit) {
if (!refillActive) {
duration=logHours-1+(float)minute()/60.0; // decimal time since oldest record
index=numHours+1<logHours?numHours+1:0;
logLevel[index]>level?delta=logLevel[index]-level:delta=0;
rate=24*(delta/duration);
} else {
refillActive=false;
rateInit=false;
}
} else {
refillActive=true;
}
if(minute()==59 && rateSaved==false) {
InternalMemory.write(Mem_B_LogATO, rate);
logLevel[numHours]=level;
rateSaved=true;
} else {
rateSaved=false;
}
ReefAngel.CustomVar[Var_LogATO]=rate;
// if(now()%30==0) Serial.println(rate);
}
void Vacation() {
boolean onVacation=InternalMemory.read(Mem_B_Vacation);
ReefAngel.Relay.Set(VO_Vacation, onVacation); // Sets RelayData only
if (ReefAngel.Relay.Status(VO_Vacation)!=onVacation) { // Looks at actual status
onVacation=ReefAngel.Relay.Status(VO_Vacation);
InternalMemory.write(Mem_B_Vacation, onVacation);
ReefAngel.Relay.Auto(VO_Vacation); // Back to auto mode
}
if (onVacation) {
if (now()%SECS_PER_DAY==20*SECS_PER_HOUR) {
ReefAngel.FeedingModeStart();
}
}
}
void LogFeedings() {
static byte feedings;
static boolean feedStatus;
if (ReefAngel.Relay.Status(Feeder)) {
feedStatus=true;
} else {
if (feedStatus) {
feedStatus=false;
feedings++;
}
}
if (ReefAngel.DisplayedMenu==FEEDING_MODE) {
if (InternalMemory.read(Mem_B_MaintFeeding) > 0) // Reset last time in Feeding Mode
InternalMemory.write(Mem_B_MaintFeeding,0);
}
if (InternalMemory.read(Mem_B_AutoFeed)) { ReefAngel.CustomVar[Var_AdjustAlk]=feedings; }
if (now()%SECS_PER_DAY==SECS_PER_DAY-1) feedings=0; // Clear feedings at end of day
}
void RunFeeder() {
boolean autoFeed;
static time_t t;
int press, repeat, offset;
autoFeed=InternalMemory.read(Mem_B_AutoFeed);
press=InternalMemory.read(Mem_B_AutoFeedPress);
repeat=InternalMemory.read(Mem_B_AutoFeedRepeat)*SECS_PER_HOUR;
offset=InternalMemory.read(Mem_B_AutoFeedOffset)*SECS_PER_HOUR;
ReefAngel.Relay.Set(VO_AutoFeed, autoFeed);
if (ReefAngel.Relay.Status(VO_AutoFeed)!=autoFeed) {
autoFeed=ReefAngel.Relay.Status(VO_AutoFeed);
InternalMemory.write(Mem_B_AutoFeed, autoFeed);
ReefAngel.Relay.Auto(VO_AutoFeed);
}
if (autoFeed) {
if (sun.IsDaytime() && (now()+offset)%repeat==0) {
t=now();
}
}
if (ReefAngel.Relay.isMaskOn(Feeder)) {
ReefAngel.Relay.Auto(Feeder);
t=now()-now()%20; // One feeding has already happened.
}
// Press the button once every 20 seconds
if (now()-t < press*20) {
ReefAngel.Relay.Set(Feeder,now()%20==0);
} else {
ReefAngel.Relay.Off(Feeder);
}
}
void RunSwabbie() {
int repeat=InternalMemory.read(Mem_B_SwabbieRepeat)*60;
int runtime=InternalMemory.read(Mem_B_SwabbieTime)*60;
static time_t t;
if (!ReefAngel.Relay.Status(VO_EnableATO)) {
// Manual mode
if (ReefAngel.Relay.isMaskOn(Swabbie)) {
ReefAngel.Relay.Auto(Swabbie);
t=now();
}
if (now()-t < runtime) {
ReefAngel.Relay.On(Swabbie);
} else {
ReefAngel.DosingPumpRepeat(Swabbie,0,repeat,runtime);
}
}
}
void SumpLights() {
int runtime=1800;
static boolean trigger=false;
static time_t t;
// Refugium Light Turned on
if (ReefAngel.Relay.isMaskOn(VO_SumpLights) && trigger==false) {
DaylightPWMValue=100;
ActinicPWMValue=100;
Daylight2PWMValue=100;
Actinic2PWMValue=100;
trigger=true;
t=now();
}
if ((!ReefAngel.Relay.Status(VO_SumpLights) || (now()-t > runtime)) && trigger==true) {
ReefAngel.Relay.Auto(VO_SumpLights);
bitClear(ReefAngel.StatusFlags,LightsOnFlag);
DaylightPWMValue=0;
ActinicPWMValue=0;
Daylight2PWMValue=0;
Actinic2PWMValue=0;
trigger=false;
}
}
// Definitions for dosing pumps
const byte numDPumps=3;
const byte pump[numDPumps]={ DPump1, DPump2, DPump3}; // Pump 3 is just for logging/calibration routine
const byte varReport[numDPumps]={ Var_DPump1, Var_DPump2, Var_DPump3};
const int memDPTime[numDPumps]={ Mem_B_DP1Timer, Mem_B_DP2Timer, Mem_B_DP3Timer};
const int memDPVolume[numDPumps]={ Mem_I_DP1Volume, Mem_I_DP2Volume, Mem_I_DP3Volume };
const int memDPRepeat[numDPumps]={ Mem_I_DP1RepeatInterval, Mem_I_DP2RepeatInterval, Mem_I_DP3RepeatInterval };
const int memCalTime[numDPumps]={ Mem_I_CalDP1Time, Mem_I_CalDP2Time, Mem_I_CalDP3Time };
const int memCalVol[numDPumps]={ Mem_I_CalDP1Vol, Mem_I_CalDP2Vol, Mem_I_CalDP3Vol };
void RunDosingPumps() {
float rate;
byte dpTime;
int dpRepeat, calcTime, totalVolume;
const int numPumps = numDPumps;
static byte doseTime[numPumps]={
InternalMemory.read(memDPTime[0]),
InternalMemory.read(memDPTime[1]),
InternalMemory.read(memDPTime[2])
};
for (int i=0;i < numPumps; i++) {
dpTime=InternalMemory.read(memDPTime[i]);
dpRepeat=InternalMemory.read_int(memDPRepeat[i]);
rate=(float)InternalMemory.read_int(memCalVol[i])/InternalMemory.read_int(memCalTime[i]);
calcTime=(InternalMemory.read_int(memDPVolume[i])/rate)/(1440/dpRepeat);
totalVolume=rate*(dpTime*(1440/dpRepeat));
/* if (i==2) {
Serial.println("Start");
Serial.println(memDPTime[2]);
Serial.println(memDPVolume[2]);
Serial.println(memDPRepeat[2]);
Serial.println(dpTime);
Serial.println(doseTime[2]);
Serial.println(calcTime);
Serial.println(dpRepeat);
Serial.println(rate);
Serial.println(totalVolume);
Serial.println(memCalTime[2]);
Serial.println(memCalVol[2]);
Serial.println("End");
}
*/
if (dpTime!=doseTime[i]) { // Memory has changed.
InternalMemory.write_int(memDPVolume[i], totalVolume); // Update volume
doseTime[i]=dpTime;
} else if (dpTime!=calcTime) { // Calculated time has changed.
InternalMemory.write(memDPTime[i], calcTime); // Update time
doseTime[i]=calcTime;
}
byte alkTarget, alkAdjVol, alkAdjDelta, adjTime;
alkTarget=InternalMemory.read(Mem_B_AlkTarget);
alkAdjVol=InternalMemory.read(Mem_B_AlkAdjVol);
alkAdjDelta=InternalMemory.read(Mem_B_AlkAdjDelta);
adjTime=doseTime[i];
if (ReefAngel.Params.PHExp/5.6 < alkTarget-1 && ReefAngel.CustomVar[Var_AdjustAlk]==alkTarget-1) {
// Increase dosage time if we're under our threshold
if (alkAdjDelta<InternalMemory.read(Mem_B_AlkMaxDelta)) {
adjTime+=alkAdjVol/rate;
} else {
ReefAngel.CustomVar[Var_AdjustAlk]=0;
}
}
if (ReefAngel.Params.PHExp/5.6 > alkTarget+1 && ReefAngel.CustomVar[Var_AdjustAlk]==alkTarget+1 ) {
// Decrease dosage time if we're over our threshold
if (alkAdjDelta>0) {
adjTime<alkAdjVol/rate ? adjTime=0 : adjTime-=alkAdjVol/rate;
} else {
ReefAngel.CustomVar[Var_AdjustAlk]=0;
}
}
// If any of these are on
if (ReefAngel.Relay.Status(VO_Calibrate) || !ReefAngel.Relay.Status(Return)) {
// Do nothing
} else {
ReefAngel.DosingPumpRepeat(pump[i], i*5, dpRepeat, adjTime);
}
}
}
void adjustAlk() {
static time_t alktime;
byte alkTarget=InternalMemory.read(Mem_B_AlkTarget);
byte alkPump=DPump2;
int alkneeded=0;
float onTime;
// check to see if extra alk was requested
// CustomVar is the value read on the checker // Added the port lock to make sure we're doing this conciously.
if (ReefAngel.CustomVar[Var_AdjustAlk] > 0 && !ReefAngel.Relay.Status(VO_LockPorts)) {
alkneeded = alkTarget - ReefAngel.CustomVar[Var_AdjustAlk];
onTime = 0;
if (alkneeded > 0 && alkneeded < 10) {
onTime = 2.19*(float)alkneeded; // onTime is minutes to run, for 70 gallons volume, 1ppm is 2.7ml and at 1.28ml/min, so 2.19 minutes per ppm of alk.
} else {
ReefAngel.CustomVar[Var_AdjustAlk] = 0; // else it must be either negative or at 150, so reset and do nothing.
return;
}
if (alktime == 0) { // it must have just gotten noticed
alktime = now()+(onTime*60UL); // should get rounded to integer seconds here
ReefAngel.Relay.On(alkPump); // set it to on
} else if (now() > alktime) {
ReefAngel.Relay.Off(alkPump); // turn it off
ReefAngel.CustomVar[Var_AdjustAlk] = 0; // clear the custom variable
alktime = 0; // set alktime back to 0
} else { // must be less than alktime, so keep it on
ReefAngel.Relay.On(alkPump);
}
} else {
return;
}
}
void LogDosingPumps() {
static time_t pumpTimer[numDPumps];
static boolean pumpStatus[numDPumps], initLog;
const byte memDPLogs[numDPumps]={ Mem_I_DP1Log, Mem_I_DP2Log, Mem_I_DP3Log};
float rate;
if (!initLog) {
initLog=true;
for (int i=0;i< numDPumps;i++) {
pumpTimer[i] = InternalMemory.read_int(memDPLogs[i]);
Serial.print("Init Pump ");
Serial.print(i);
Serial.print(":");
Serial.println(pumpTimer[i]);
rate=(float)InternalMemory.read_int(memCalVol[i])/InternalMemory.read_int(memCalTime[i]);
ReefAngel.CustomVar[varReport[i]]=pumpTimer[i]*rate;
if (i==1) {
ReefAngel.CustomVar[Var_Baseline]=rate*(InternalMemory.read(memDPTime[1])*(NumMins(hour(),minute())/InternalMemory.read(memDPRepeat[1])));
}
}
}
for (int i=0;i< numDPumps;i++) {
if (ReefAngel.Relay.Status(pump[i])) {
if (!pumpStatus[i]) {
pumpTimer[i]=now()-pumpTimer[i]; // Pump was off, timer is now a time
pumpStatus[i]=true;
}
} else {
if (pumpStatus[i]) {
pumpTimer[i]=now()-pumpTimer[i]; // Pump was on, timer is now a timer
pumpStatus[i]=false;
rate=(float)InternalMemory.read_int(memCalVol[i])/InternalMemory.read_int(memCalTime[i]);
ReefAngel.CustomVar[varReport[i]]=pumpTimer[i]*rate;
byte alkAdjDelta;
alkAdjDelta=InternalMemory.read(Mem_B_AlkAdjDelta);
if (i==1) {
// Write Baseline
ReefAngel.CustomVar[Var_Baseline]=rate*(InternalMemory.read(memDPTime[1])*(NumMins(hour(),minute())/InternalMemory.read(memDPRepeat[1])));
// Reset CustomVar as dosing adjustment is now complete. Increment the delta
if (ReefAngel.CustomVar[Var_AdjustAlk] == InternalMemory.read(Mem_B_AlkTarget)+1) {
alkAdjDelta-2 < 0 ? alkAdjDelta=0 : alkAdjDelta-=2;
InternalMemory.write(Mem_B_AlkAdjDelta, alkAdjDelta);
ReefAngel.CustomVar[Var_AdjustAlk]=0;
} else if (ReefAngel.CustomVar[Var_AdjustAlk] == InternalMemory.read(Mem_B_AlkTarget)-1) {
InternalMemory.write(Mem_B_AlkAdjDelta, alkAdjDelta+2);
ReefAngel.CustomVar[Var_AdjustAlk]=0;
} else {
if (alkAdjDelta > 0) InternalMemory.write(Mem_B_AlkAdjDelta,alkAdjDelta-1);
}
}
InternalMemory.write_int(memDPLogs[i],pumpTimer[i]);
Serial.print("Writing Pump ");
Serial.print(i);
Serial.print(":");
Serial.println(pumpTimer[i]);
}
}
/* Serial.print(rate, 4); Serial.print(" * ");
Serial.print(InternalMemory.read(memDPTime[1])); Serial.print(" * (");
Serial.print(NumMins(hour(now()-300),minute(now()-300))); Serial.print(" / ");
Serial.println(InternalMemory.read(memDPRepeat[1]));
*/
// Maintenence routine
if (now()%SECS_PER_DAY==SECS_PER_DAY-1) {
pumpTimer[i]=0; // Clear timer at end of day
ReefAngel.CustomVar[varReport[i]]=0; // Clear portal variable
ReefAngel.CustomVar[Var_Baseline]=0;
InternalMemory.write_int(memDPLogs[i],pumpTimer[i]);
}
}
// See if we are dosing vinegar and adjust dosage each week.
byte vinegarWeek=InternalMemory.read(Mem_B_VinegarWeek);
static boolean vinegarCounter=false;
if (dayOfWeek(now())!=2) vinegarCounter=true;
if (dayOfWeek(now())==2 && vinegarCounter && vinegarWeek<=16) {
vinegarWeek++;
vinegarCounter=false;
// Update Mem_I_DP3Volume and add 4ml.
InternalMemory.write_int(Mem_I_DP3Volume, InternalMemory.read_int(Mem_I_DP3Volume)+4);
// Update Vinegar week in memory.
InternalMemory.write(Mem_B_VinegarWeek,vinegarWeek);
}
}
void CalibrateDPumps() {
static time_t pumpTimer[numDPumps];
static boolean pumpStatus[numDPumps];
static boolean running=false;
if (ReefAngel.Relay.Status(VO_Calibrate)) {
running=true;
for (int i=0;i < numDPumps;i++) {
if (ReefAngel.Relay.Status(pump[i])) {
if (!pumpStatus[i]) {
pumpTimer[i]=now()-pumpTimer[i]; // Pump was off, timer is now a time
pumpStatus[i]=true;
}
} else {
if (pumpStatus[i]) {
pumpTimer[i]=now()-pumpTimer[i]; // Pump was on, timer is now a timer
pumpStatus[i]=false;
}
}
}
} else {
if (running) {
running=false;
for (int i=0;i < numDPumps;i++) {
if (pumpTimer[i]>0 && !ReefAngel.Relay.Status(VO_LockPorts)) {
InternalMemory.write_int(memCalTime[i], pumpTimer[i]);
}
ReefAngel.Relay.Auto(pump[i]); // Go back to auto mode
pumpStatus[i]=false;
pumpTimer[i]=0;
}
}
}
}
void AutoWaterChange() {
int runtime=InternalMemory.read_int(Mem_I_WCFillTime);
static WiFiAlert wcAlert;
static boolean started;
static time_t t;
if (ReefAngel.DisplayedMenu==WATERCHANGE_MODE) {
ReefAngel.Relay.On(Refugium);
if (ReefAngel.Relay.Status(VO_EnableATO)) {
ReefAngel.SingleATO(false,Swabbie,30,InternalMemory.ATOHourInterval_read()); // Refill fresh SW as needed
if (InternalMemory.read(Mem_B_MaintWC) > 0) // Reset last WC counter
InternalMemory.write(Mem_B_MaintWC,0);
if (ReefAngel.Relay.Status(VO_StartFill) && !started) {
ReefAngel.Relay.Override(Reactor,1); // Start draining
started=true;
t=now();
}
if ( (now()-t > runtime && started) || bitRead(ReefAngel.AlertFlags,ATOTimeOutFlag) ) {
ReefAngel.Relay.Override(Reactor,0); // Stop draining
ReefAngel.Relay.Auto(VO_StartFill); // Reset Start switch
if (started) wcAlert.Send("Reactor+disabled.", true);
started=false;
}
if (wcAlert.IsActive()) wcAlert.Send();
} else {
// Done with ATO
ReefAngel.Relay.Off(Swabbie);
// Clear stale timeout
ReefAngel.ATOClear();
}
} else {
ReefAngel.Relay.Auto(VO_EnableATO);
ReefAngel.Relay.Auto(VO_StartFill);
}
}
void RunMixingStation() {
int mixTime=InternalMemory.read_int(Mem_I_MixTime);
int mixFreq=InternalMemory.read_int(Mem_I_MixFrequency);
int awcTime=InternalMemory.read_int(Mem_I_AWCTime);
int awcFreq=InternalMemory.read_int(Mem_I_AWCFrequency);
int awcOffset=InternalMemory.read_int(Mem_I_AWCOffset);
byte flushTime=InternalMemory.read(Mem_B_FlushTime);
static time_t t;
ReefAngel.Relay.Set(MixPump,now()%mixFreq<mixTime);
ReefAngel.Relay.Set(AWCPump,now()%awcFreq+awcOffset<awcTime);
if (ReefAngel.Relay.isMaskOn(ROSolenoid)) {
ReefAngel.Relay.Auto(ROSolenoid);
t=now();
}
if (now()-t < flushTime) {
ReefAngel.Relay.On(ROSolenoid);
} else {
ReefAngel.Relay.Off(ROSolenoid);
}
// Turn on the RelayPS when needed.
ReefAngel.Relay.Set(RelayPS, (ReefAngel.Relay.Status(ROSolenoid) || ReefAngel.Relay.Status(AWCPump)));
}
// Disable masks for things that should not be turned on by mistake
void LockPorts() {
ReefAngel.AddPortOverrides();
if (ReefAngel.Relay.Status(VO_LockPorts)) {
ReefAngel.OverridePortsE[0] = Port1Bit | Port3Bit | Port5Bit; // Dosing Pumps 6,7,8
ReefAngel.OverridePortsE[1] = Port4Bit; // ATO Solenoid
ReefAngel.OverridePortsE[2] = Port4Bit | Port5Bit | Port7Bit; // Vacaton/AutoFeed/Calibrate
} else {
ReefAngel.OverridePortsE[0] = 0;
ReefAngel.OverridePortsE[1] = 0;
ReefAngel.OverridePortsE[2] = 0;
}
}
void Reminders() {
static boolean counterReady;
byte counter[]= { Mem_B_MaintGAC, Mem_B_MaintGFO, Mem_B_MaintCal, Mem_B_MaintAlk,
Mem_B_MaintWC, Mem_B_MaintATO, Mem_B_MaintFeeding, Mem_B_MaintSkimmer, Mem_B_MaintSocks, Mem_B_MaintVinegar };
if (now()%SECS_PER_DAY!=0) counterReady=true;
if (now()%SECS_PER_DAY==0 && counterReady) {
for (int i=0;i<sizeof(counter);i++) {
InternalMemory.write(counter[i],InternalMemory.read(counter[i])+1);
}
counterReady=false;
}
}
void DailyReport() {
static WiFiAlert dailyReport;
static boolean sent;
static char msg[64];
char temp[10];
char ph[10];
char alk[10];
char sal[10];
if ((now()+(6*SECS_PER_HOUR))%(12*SECS_PER_HOUR)==5 && !sent) {
dtostrf((float)ReefAngel.Params.Temp[T1_PROBE]/10,3, 1, temp);
dtostrf((float)ReefAngel.Params.PH/100,4, 2, ph);
dtostrf((float)ReefAngel.Params.PHExp/100,4, 2, alk);
dtostrf((float)ReefAngel.Params.Salinity/10,3, 1, sal);
sprintf(msg,"T1:%s+PH:%s+KH:%s+S:%s+WL:%d",temp,ph,alk,sal,ReefAngel.WaterLevel.GetLevel());
dailyReport.Send(msg,true);
sent=true;
} else {
if (dailyReport.IsActive()) {
dailyReport.Send();
} else {
sent=false;
}
}
}
void DelayedOnModes(byte relay) {
static unsigned long startTime=now();
if ( (startTime==LastStart) && ReefAngel.HighATO.IsActive()) {
ReefAngel.Relay.On(relay);
} else {
ReefAngel.Relay.DelayedOn(relay);
}
}
// ------------------------------------------------------------
// Change the values below to customize your cloud/storm effect
// Frequency in days based on the day of the month - number 2 means every 2 days, for example (day 2,4,6 etc)
// For testing purposes, you can use 1 and cause the cloud to occur everyday
#define Clouds_Every_X_Days 1
// Percentage chance of a cloud happening today
// For testing purposes, you can use 100 and cause the cloud to have 100% chance of happening
#define Cloud_Chance_per_Day 50
// Minimum number of minutes for cloud duration. Don't use min duration of less than 6
#define Min_Cloud_Duration 10
// Maximum number of minutes for the cloud duration. Don't use max duration of more than 255
#define Max_Cloud_Duration 20
// Minimum number of clouds that can happen per day
#define Min_Clouds_per_Day 1
// Maximum number of clouds that can happen per day
#define Max_Clouds_per_Day 4
// Only start the cloud effect after this setting
// In this example, start cloud after noon
#define Start_Cloud_After NumMins(9,00)
// Always end the cloud effect before this setting
// In this example, end cloud before 9:00pm
#define End_Cloud_Before NumMins(23,00)
// Percentage chance of a lightning happen for every cloud
// For testing purposes, you can use 100 and cause the lightning to have 100% chance of happening
#define Lightning_Chance_per_Cloud 65
// Note: Make sure to choose correct values that will work within your PWMSLope settings.
// For example, in our case, we could have a max of 5 clouds per day and they could last for 50 minutes.
// Which could mean 250 minutes of clouds. We need to make sure the PWMSlope can accomodate 250 minutes
// of effects or unforseen result could happen.
// Also, make sure that you can fit double those minutes between Start_Cloud_After and End_Cloud_Before.
// In our example, we have 510 minutes between Start_Cloud_After and End_Cloud_Before, so double the
// 250 minutes (or 500 minutes) can fit in that 510 minutes window.
// It's a tight fit, but it did.
//#define printdebug // Uncomment this for debug print on Serial Monitor window
#define forcecloudcalculation // Uncomment this to force the cloud calculation to happen in the boot process.
// Add Random Lightning modes
#define Calm 0 // No lightning
#define Slow 1 // 5 seconds of slow lightning in the middle of a cloud for ELN style (slow response) drivers
#define Fast 2 // 5 seconds of fast lightning in the middle of a cloud for LDD style (fast response) drivers
#define Mega 3 // Lightning throughout the cloud, higher chance as it gets darker
#define Mega2 4 // Like Mega, but with more lightning
// ------------------------------------------------------------
// Do not change anything below here
static byte cloudchance=255;
static byte cloudduration=0;
static int cloudstart=0;
static byte numclouds=0;
static byte lightningchance=0;
static byte cloudindex=0;
static byte lightningstatus=0;
static byte lightningMode=0;
static boolean chooseLightning=true;
void CheckCloud()
{
// Set which modes you want to use
// Example: { Calm, Fast, Mega, Mega2 } to randomize all four modes.
// { Mega2 } for just Mega2. { Mega, Mega, Fast} for Mega and Fast, with twice the chance of Mega.
byte LightningModes[] = {Slow};
// Change the values above to customize your cloud/storm effect
static time_t DelayCounter=millis(); // Variable for lightning timing.
static int DelayTime=random(1000); // Variable for lightning timimg.
// Every day at midnight, we check for chance of cloud happening today
if (hour()==0 && minute()==0 && second()==0) cloudchance=255;
#ifdef forcecloudcalculation
if (cloudchance==255)
#else
if (hour()==0 && minute()==0 && second()==1 && cloudchance==255)
#endif
{
// Commenting out to see if it's interfering with our other seed.
// randomSeed(millis()); // Seed the random number generator
//Pick a random number between 0 and 99
cloudchance=random(100);
// if picked number is greater than Cloud_Chance_per_Day, we will not have clouds today
if (cloudchance>Cloud_Chance_per_Day) cloudchance=0;
// Check if today is day for clouds.
if ((day()%Clouds_Every_X_Days)!=0) cloudchance=0;
// If we have cloud today
if (cloudchance)
{
// pick a random number for number of clouds between Min_Clouds_per_Day and Max_Clouds_per_Day
numclouds=random(Min_Clouds_per_Day,Max_Clouds_per_Day);
// pick the time that the first cloud will start
// the range is calculated between Start_Cloud_After and the even distribuition of clouds on this day.
cloudstart=random(Start_Cloud_After,Start_Cloud_After+((End_Cloud_Before-Start_Cloud_After)/(numclouds*2)));
// pick a random number for the cloud duration of first cloud.
cloudduration=random(Min_Cloud_Duration,Max_Cloud_Duration);
//Pick a random number between 0 and 99
lightningchance=random(100);
// if picked number is greater than Lightning_Chance_per_Cloud, we will not have lightning today
if (lightningchance>Lightning_Chance_per_Cloud) lightningchance=0;
}
}
// Now that we have all the parameters for the cloud, let's create the effect
if (!InternalMemory.read(Mem_B_EnableStorm)) return;
if (cloudchance)
{
if (ReefAngel.Relay.isMaskOff(LED_STORM)) // Change this to whatever port you want to use as a trigger.
{
cloudstart = NumMins(hour(), minute());
ReefAngel.Relay.Auto(LED_STORM); // Here, too.
}
//is it time for cloud yet?
if (NumMins(hour(),minute())>=cloudstart && NumMins(hour(),minute())<(cloudstart+cloudduration))
{
// Cloud Dimming
DaylightPWMValue0=ReversePWMSlopeHighRes(cloudstart,cloudstart+cloudduration,DaylightPWMValue0,100,240);
DaylightPWMValue2=ReversePWMSlopeHighRes(cloudstart,cloudstart+cloudduration,DaylightPWMValue2,100,240);
if (chooseLightning)
{
lightningMode=LightningModes[random(100)%sizeof(LightningModes)];
chooseLightning=false;
}
switch (lightningMode)
{
case Calm:
break;
case Mega:
// Lightning chance from beginning of cloud through the end. Chance increases with darkness of cloud.
if (lightningchance && random(ReversePWMSlope(cloudstart,cloudstart+cloudduration,100,0,180))<1 && (millis()-DelayCounter)>DelayTime)
{
// Send the trigger
Strike();
DelayCounter=millis(); // If we just had a round of flashes, then lets put in a longer delay
DelayTime=random(1000); // of up to a second for dramatic effect before we do another round.
}
break;
case Mega2:
// Higher lightning chance from beginning of cloud through the end. Chance increases with darkness of cloud.
if (lightningchance && random(ReversePWMSlope(cloudstart,cloudstart+cloudduration,100,0,180))<2)
{
Strike();
}
break;
case Fast:
// 5 seconds of lightning in the middle of the cloud
if (lightningchance && (NumMins(hour(),minute())==(cloudstart+(cloudduration/2))) && second()<5 && (millis()-DelayCounter)>DelayTime)
{
Strike();
DelayCounter=millis(); // If we just had a round of flashes, then lets put in a longer delay
DelayTime=random(1000); // of up to a second for dramatic effect before we do another round.
}
break;
case Slow:
// Slow lightning for 5 seconds in the middle of the cloud. Suitable for slower ELN style drivers
if (lightningchance && second()%40<8)
{
SlowStrike();
}
break;
default:
break;
}
}
else
{
chooseLightning=true; // Reset the flag to choose a new lightning type
}
if (NumMins(hour(),minute())>(cloudstart+cloudduration))
{
cloudindex++;
if (cloudindex < numclouds)
{
cloudstart=random(Start_Cloud_After+(((End_Cloud_Before-Start_Cloud_After)/(numclouds*2))*cloudindex*2),(Start_Cloud_After+(((End_Cloud_Before-Start_Cloud_After)/(numclouds*2))*cloudindex*2))+((End_Cloud_Before-Start_Cloud_After)/(numclouds*2)));
// pick a random number for the cloud duration of first cloud.
cloudduration=random(Min_Cloud_Duration,Max_Cloud_Duration);
//Pick a random number between 0 and 99
lightningchance=random(100);
// if picked number is greater than Lightning_Chance_per_Cloud, we will not have lightning today
if (lightningchance>Lightning_Chance_per_Cloud) lightningchance=0;
}
}
}
// Cloud ON option - Clouds every minute
if (ReefAngel.Relay.isMaskOn(LED_STORM) && now()%60<10)
{
SlowStrike();
}
}
void SlowStrike()
{
int r = random(100);
if (r<20) lightningstatus=1;
else lightningstatus=0;
if (lightningstatus)
{
// Let's separate left and right both.
if (r<13) {
DaylightPWMValue0=4095;
DaylightPWMValue2=4095;
} else if (r<17) {
DaylightPWMValue0=100;
DaylightPWMValue2=4095;
} else {
DaylightPWMValue0=4095;
DaylightPWMValue2=100;
}
}
else
{
DaylightPWMValue0=100;
DaylightPWMValue2=100;
}
delay(2);
}
void Strike()
{
int a=random(1,5); // Pick a number of consecutive flashes from 1 to 4.
for (int i=0; i<a; i++)
{
// Flash on
int newdata=4095;
Wire.beginTransmission(0x40); // Address of the dimming expansion module
Wire.write(0x8+(4*0)); // 0x8 is channel 0, 0x12 is channel 1, etc. This is channel 0.
Wire.write(newdata&0xff); // Send the data 8 bits at a time. This sends the LSB
Wire.write(newdata>>8); // This sends the MSB
Wire.endTransmission();
Wire.beginTransmission(0x40); // Address of the dimming expansion module
Wire.write(0x8+(4*2)); // 0x8 is channel 0, 0x12 is channel 1, etc. This is channel 2.
Wire.write(newdata&0xff); // Send the data 8 bits at a time. This sends the LSB
Wire.write(newdata>>8); // This sends the MSB
Wire.endTransmission();
int randy=random(20,80); // Random number for a delay
if (randy>71) randy=((randy-70)/2)*100; // Small chance of a longer delay
delay(randy); // Wait from 20 to 69 ms, or 100-400 ms
// Flash off. Return to baseline.
newdata=ReefAngel.PWM.GetChannelValueRaw(0); // Use the channel number you're flashing here
Wire.beginTransmission(0x40); // Same as above
Wire.write(0x8+(4*0));
Wire.write(newdata&0xff);
Wire.write(newdata>>8);
Wire.endTransmission();
newdata=ReefAngel.PWM.GetChannelValueRaw(2); // Use the channel number you're flashing here
Wire.beginTransmission(0x40); // Same as above
Wire.write(0x8+(4*2));
Wire.write(newdata&0xff);
Wire.write(newdata>>8);
Wire.endTransmission();
delay(random(30,50)); // Wait from 30 to 49 ms
wdt_reset(); // Reset watchdog timer to avoid re-boots
}
}
byte ReversePWMSlope(long cstart,long cend,byte PWMStart,byte PWMEnd, byte clength)
{
long n=elapsedSecsToday(now());
cstart*=60;
cend*=60;
if (n<cstart) return PWMStart;
if (n>=cstart && n<=(cstart+clength)) return map(n,cstart,cstart+clength,PWMStart,PWMEnd);
if (n>(cstart+clength) && n<(cend-clength)) return PWMEnd;
if (n>=(cend-clength) && n<=cend) return map(n,cend-clength,cend,PWMEnd,PWMStart);
if (n>cend) return (int) PWMStart;
}
int ReversePWMSlopeHighRes(long cstart,long cend,int PWMStart,int PWMEnd, byte clength)
{
long n=elapsedSecsToday(now());
cstart*=60;
cend*=60;
if (n<cstart) return PWMStart;
if (n>=cstart && n<=(cstart+clength)) return map(n,cstart,cstart+clength,PWMStart,PWMEnd);
if (n>(cstart+clength) && n<(cend-clength)) return PWMEnd;
if (n>=(cend-clength) && n<=cend) return map(n,cend-clength,cend,PWMEnd,PWMStart);
if (n>cend) return (int) PWMStart;
}
You no longer need any extra classes to use this code with the current libraries: 1.1.1