Lee's Feature Complete PDE

Share you PDE file with our community
User avatar
Posts: 5430
Joined: Fri Jul 20, 2012 9:42 am
PostPosted: Fri Dec 28, 2012 9:56 pm
So I've been maintaining my code in the For New Members forum and I think it's time to create a proper thread in the main PDE/INO forum.

This PDE is pretty feature complete, and I've been using my RA+ for over 4 months now. Hopefully this will be a good base for people trying to implement some of these features or utilize this hardware.

Current RA hardware inventory:
RA+
RF module
WiFi module
Salinity module
Expansion Hub
Relay Expansion
Water Level Sensor
3 Temp Sensors (Display, Sump, Stand)
2 Float switches (monitoring return chamber)

Here is a summary of my software features:

Custom feeding mode
    Start:
    Pump / Skimmer / Reactor off
    MP40s to Feeding Mode
    End:
    Pump and Reactor On
    Skimmer on after 5 minutes
    Post:
    15 minutes later MP40s to Nutrient Transport mode

Heater Control
Dosing Pumps - for 2-part Cal/Alk + Vinegar
Sump light on during WaterChange mode.
Toggle Sump light from RA panel
Moonlights based on current MoonPhase
Sun and Moon rise/set based on GPS coordinates
Vortech Menu - Change modes from the RA.
Vacation Mode - Use my spare pump to top-off my ATO reservoir when away longer than 5 days
ATO Refill mode - Use my spare pump to refill my top-off reservoir to 100% for easy refills
Coral Acclimation - Automate an acclimation schedule for new corals!
PowerOutage handling - Disable unused devices if power is lost (Main relay on UPS, Expansion relay non-UPS)
Multiple screens on display

I have a Gravity Fed ATO so I'm using my float switches and WaterLevel sensor for the following:
Avast Marine Skimmate locker float switch used to shutdown skimmer if skimmate collector is full (plugged into ATOLow)
One float switch used to shutdown return pump if sump is overflowing
The other float switch is also used to shutdown return pump if sump is low on water (both switches wired in parallel)
WaterLevel sensor to monitor ATO reservoir capacity

Updated: 3/11/13
Automated Water Changes!!!!
Tidal Simulation and Custom MP40 mode!!

Updated: 4/29/13
AutoFeeder
Swabbie Control
Reverse ATO Switches
Port Locking (no accidental dosing!)
Initialize my Custom Memory

Updated: 5/30/13
Virtual Outlets
Dosing by volume (and pump calibration)
WiFiAlerts

Updated: 6/3/2013
Monitor ATO depletion rate
Relay On/Off Log (tracking my auto-feeder)

Updated: 6/29/2013
BioHazard Mode
Else Mode

Updated: 7/6/2013
Reminders (track maintenance tasks)

Updated: 7/20/2013
DelayedOnModes() function
Fine tuned pulse mode timing
Added reminder for Filter socks

Update: 9/18/2013
Fixed AutoWaterChange to accomodate new methods.
Minor tweaks (i.e. forgot what I touched..)

Update: 2/1/2015
RF FeedingMode speed
Acclimation code for dimming
Adjust alk manually (thanks AlanM)
Daily email report
Light modes (select in memory which wavetype for lighting)
Filll in gap from 5%->0% with moonlights

Update: 7/13/2015
Vinegar Dosing - Increase dose each week
LED Presets
Clouds and Lightning

Update: 6/17/2016
Turn off fuge light after 2 hours (I always forget to turn it off)
Improvements to evaporation measurement

Update: 2/17/2019
Wow...been a while...
Variable alk dosing
Mixing station
Moved to Star
Persistent logging for dosing pumps
Bug fix to lighting scheduling offsets (Libraries updated as well)

If you have any questions or comments, feel free to ask!

Lee
Last edited by lnevo on Fri Dec 28, 2012 10:27 pm, edited 6 times in total.
User avatar
Posts: 5430
Joined: Fri Jul 20, 2012 9:42 am
PostPosted: Fri Dec 28, 2012 9:56 pm
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
Last edited by lnevo on Mon Jan 07, 2013 10:30 pm, edited 8 times in total.
User avatar
Posts: 5430
Joined: Fri Jul 20, 2012 9:42 am
PostPosted: Fri Dec 28, 2012 10:02 pm
Here's some info on my setup. I started this tank on July 14th, 2012. It is my first saltwater tank.

My tank is currently a 65 gallon mixed reef.

Deep Blue 65gallon tank - 36x18x24
Eshopps R-100 refugium / sump
Eshopps PSK-100 skimmer w/swabbie
Avast Marine Davey Jones Skimmate Locker
Deep Blue Triton-3 650gph return pump
ReefOctopus MF300-B media reactor x2
Fluval Power Compact refugium light
200 Watt Titanium heater
2 Vortech MP40WES
120 Watt LED fixture (LFS branded)
D-Link DCS-930L webcam
2 BRS 1.1mL/min dosers
Eheimm AutoFeeder

Current fish list:
2 Ocellaris clownfish
1 Royal Gramma
1 Firefish
1 Orange-Spot Diamond Goby
1 One-Spot Foxface
1 Yellow Coris Wrasse
1 Blue Hippo Tang
3 Orange-line Chromis
Last edited by lnevo on Fri Dec 28, 2012 10:15 pm, edited 3 times in total.
User avatar
Posts: 5430
Joined: Fri Jul 20, 2012 9:42 am
PostPosted: Fri Dec 28, 2012 10:03 pm
ImageImageImage
User avatar
Posts: 2871
Joined: Fri Mar 18, 2011 6:20 pm
Location: Illinois
PostPosted: Sat Dec 29, 2012 8:27 am
Awesome! Great write up.

Posts: 12877
Joined: Fri Mar 18, 2011 6:47 pm
PostPosted: Sun Dec 30, 2012 10:25 am
Very cool!!! 8-)
Roberto.
User avatar
Posts: 5430
Joined: Fri Jul 20, 2012 9:42 am
PostPosted: Mon Dec 31, 2012 3:33 pm
Oops, fixed a bug... I was refilling my ATO until WaterLevel<=100... The problem is that WaterLevel() never goes over 100% :)

Had a little spill... not from that function directly, since I was testing it in action tonight, but because I used the pump to create a siphon and drain out some of the water... well, then I turned away because I thought I was ok...but guess what... pumped turned back on because the water level was back below 99 again, and didn't shut off!

Well, at least I was there and caught it. Nothing a towel couldn't soak up... at least the function is fixed. But hopefully someone else learns something from my little lesson. Remember that WaterLevel() will never go over 100%!!! :)

Posts: 12877
Joined: Fri Mar 18, 2011 6:47 pm
PostPosted: Mon Dec 31, 2012 4:19 pm
Do you think it should?
Phrusher sent me this pull request:
https://github.com/reefangel/Libraries/pull/57
I'm debating whether it is something really useful.
The way I think is that it shouldn't go above 100%, since 100% is the top of the pipe.
Roberto.
User avatar
Posts: 5430
Joined: Fri Jul 20, 2012 9:42 am
PostPosted: Mon Dec 31, 2012 4:29 pm
You may not calibrate 100% at the top of the pipe... My pipe is mounted in my ATO reservoir, and I calibrated 100 to where the reservoir is filled (but not completely...) I just lowered the amount a bit so it wouldn't be as close as it was. It would be good to see that it is over the 100%... especially if automating functions based on that... I guess whatever (max-min) / 100 would be 1% so anything over max could be calculated relatively easily.. I don't see any reason to throw out the extra if it can be measured...

Posts: 12877
Joined: Fri Mar 18, 2011 6:47 pm
PostPosted: Mon Dec 31, 2012 4:42 pm
Ahhh.. That's the reason why you would want to measure above 100%... Your calibration is not at the top... Now I get the idea.
The way I was thinking would be you would calibrate the full pipe to 100%, but you would only be using for example 80% as full ATO reservoir.
But calibrating it to 100% as full ATO does make sense and that would definitely cause more than 100%.
I'll apply the patch on the next library update.
Roberto.
Next

Return to My PDE/INO file

Who is online

Users browsing this forum: No registered users and 1 guest