Lee's Feature Complete PDE

Share you PDE file with our community
User avatar
Posts: 5356
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 alway sforget to turn it off)
Improvements to evaporation measurement

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: 5356
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 <ReefAngel_Features.h>
#include <Globals.h>
#include <RA_Wifi.h>
#include <Wire.h>
#include <OneWire.h>
#include <Time.h>
#include <DS1307RTC.h>
#include <InternalEEPROM.h>
#include <RA_NokiaLCD.h>
#include <RA_ATO.h>
#include <RA_Joystick.h>
#include <LED.h>
#include <RA_TempSensor.h>
#include <Relay.h>
#include <RA_PWM.h>
#include <Timer.h>
#include <Memory.h>
#include <RA_Colors.h>
#include <RA_CustomColors.h>
#include <Salinity.h>
#include <RF.h>
#include <ReefAngel.h>
#include <SunLocation.h>
#include <WaterLevel.h>
#include <Tide.h>
#include <Moon.h>
#include <WiFiAlert.h>
#include <DCPump.h>

// Won't compile without this...
// ReefAngel.DCPump.UseMemory=true; 
// Custom menus
#include <avr/pgmspace.h>
prog_char menu1_label[] PROGMEM = "Refugium Light";
prog_char menu2_label[] PROGMEM = "Feeding Mode";
prog_char menu3_label[] PROGMEM = "Water Change";
prog_char menu4_label[] PROGMEM = "Vortech Mode";
prog_char menu5_label[] PROGMEM = "ATO Clear";
prog_char menu6_label[] PROGMEM = "Overheat Clear";
prog_char menu7_label[] PROGMEM = "PH Calibration";
prog_char menu8_label[] PROGMEM = "WLS Calibration";
prog_char menu9_label[] PROGMEM = "Date / Time";

// Group the menu entries together
PROGMEM const char *menu_items[] = {
menu1_label, menu2_label, menu3_label,
menu4_label, menu5_label, menu6_label,
menu7_label, menu8_label, menu9_label
};

// 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
#define Mem_B_RateAlarm       142
#define Mem_B_TideMode        143
#define Mem_B_MaintGAC        144
#define Mem_B_MaintGFO        145
#define Mem_B_MaintCal        146
#define Mem_B_MaintAlk        147
#define Mem_B_MaintWC         148
#define Mem_B_MaintATO        149
#define Mem_B_MaintFeeding    150
#define Mem_B_MaintSkimmer    151
#define Mem_B_MaintSocks      152
#define Mem_B_MaintWCVol      153
#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
#define Mem_B_EnableStorm     178
#define Mem_B_ForceRandomTide 179

#define Mem_B_PrintDebug      198
#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,true);
  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_RateAlarm,0);
  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_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 NUMBERS_8x16

// Define Portal Variables
#define Var_Feedings     0
#define Var_DPump1       1
#define Var_DPump2       2
#define Var_DPump3       3
#define Var_Tide         4
#define Var_TideMode     5
#define Var_LogATO       6
#define Var_AlkAdjust    7

// Define Relay Ports by Name
#define Return             1
#define Skimmer            2
#define WhiteLED           3
#define BlueLED            4
#define Vortechs           5
#define Heater             6
#define Refugium           7
#define Reactor            8

#define Swabbie            Box1_Port1
#define Feeder             Box1_Port2
#define Fan                Box1_Port3
#define Unused             Box1_Port4
#define ROSolenoid         Box1_Port5
#define DPump3             Box1_Port6
#define DPump1             Box1_Port7
#define DPump2             Box1_Port8

#define VO_RefillATO       Box2_Port1
#define VO_EnableATO       Box2_Port2
#define VO_StartFill       Box2_Port3
#define VO_Vacation        Box2_Port4
#define VO_AutoFeed        Box2_Port5
#define VO_PowerOut        Box2_Port6
#define VO_Calibrate       Box2_Port7
#define VO_LockPorts       Box2_Port8

////// Place global variable code below here

// Custom classes
SunLocation sun;
Tide tide;

// Vortech Variables
byte vtMode, vtSpeed, vtDuration;

// For Cloud and preset code
int DaylightPWMValue=0;
int ActinicPWMValue=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

// Needs to be global for DrawCustomGraph()
int ScreenID=1;
////// Place global variable code above here

// Setup on controller startup/reset
void setup()
{
  // This must be the first line
  ReefAngel.Init();  //Initialize controller
  ReefAngel.InitMenu(pgm_read_word(&(menu_items[0])),SIZE(menu_items)); // Initialize Menu
  ReefAngel.AddSalinityExpansion();  // Salinity Expansion Module
  ReefAngel.Salinity.SetCompensation(0);

  // Ports toggled in Feeding Mode
  ReefAngel.FeedingModePorts = Port1Bit | Port2Bit | Port8Bit;
  // Ports toggled in Water Change Mode
  ReefAngel.WaterChangePorts = Port2Bit | Port8Bit;
  // Ports toggled when Lights On / Off menu entry selected
  ReefAngel.LightsOnPorts = Port3Bit | Port4Bit;
  // Ports turned off when Overheat temperature exceeded
  ReefAngel.OverheatShutoffPorts = Port6Bit;
  // Use T1 probe as temperature and overheat functions
  ReefAngel.TempProbe = T1_PROBE;
  ReefAngel.OverheatProbe = T2_PROBE;
   
  // Ports that default on
  ReefAngel.Relay.On(Return);
  ReefAngel.Relay.On(Vortechs);
  ReefAngel.Relay.On(VO_LockPorts);     
 
  // Ports that default off
  ReefAngel.Relay.Off(ROSolenoid);
  ReefAngel.Relay.Off(Swabbie);
  ReefAngel.Relay.Off(Feeder);
   
  ////// Place additional initialization code below here
  ReefAngel.RF.UseMemory=false;
  randomSeed(now()/SECS_PER_DAY);
 
  Serial.println("Booting.");
 
  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);
  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

  RefugiumLight();        // 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

  // This should always be the last line
  ReefAngel.Portal("lnevo");
  ReefAngel.ShowInterface();

}

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<300);
    }
    ReefAngel.Relay.Off(Heater);
    ReefAngel.Relay.Off(Skimmer);
    ReefAngel.Relay.Off(Reactor);
    ReefAngel.Relay.Off(Refugium);
    ReefAngel.Relay.Off(WhiteLED);
    ReefAngel.Relay.Off(BlueLED);
    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);
  }
}

void CheckSwitches() {
  static boolean returnOverride=true;
  static WiFiAlert switchAlert, skimmerAlert, atoAlert;
  skimmerAlert.SetDelay(3600);
  atoAlert.SetDelay(3600);
 
  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(); // swap switch behavior so we can reuse as ATO in our WC function
  if (!ReefAngel.HighATO.IsActive()) { // switch on by default
    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.LowATO.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(lightOffset,0);
      ReefAngel.PWM.Channel2PWMSlope(0,lightOffset);
      // Actinics
      ReefAngel.PWM.Channel1PWMSlope(actinicOffset+lightOffset,actinicOffset);
      ReefAngel.PWM.Channel3PWMSlope(actinicOffset,actinicOffset+lightOffset);
      break;
    }
    case 1: {
      // Daylights
      ReefAngel.PWM.Channel0PWMParabola(lightOffset,0);
      ReefAngel.PWM.Channel2PWMParabola(0,lightOffset);
      // Actinics
      ReefAngel.PWM.Channel1PWMParabola(actinicOffset+lightOffset,actinicOffset);
      ReefAngel.PWM.Channel3PWMParabola(actinicOffset,actinicOffset+lightOffset);
      break;
    }
  case 2: {
      // Daylights
      ReefAngel.PWM.Channel0PWMSmoothRamp(lightOffset,0);
      ReefAngel.PWM.Channel2PWMSmoothRamp(0,lightOffset);
      // Actinics
      ReefAngel.PWM.Channel1PWMSmoothRamp(actinicOffset+lightOffset,actinicOffset);
      ReefAngel.PWM.Channel3PWMSmoothRamp(actinicOffset,actinicOffset+lightOffset);
      break;
    }
  case 3: {
      // Daylights
      ReefAngel.PWM.Channel0PWMSigmoid(lightOffset,0);
      ReefAngel.PWM.Channel2PWMSigmoid(0,lightOffset);
      // Actinics
      ReefAngel.PWM.Channel1PWMSigmoid(actinicOffset+lightOffset,actinicOffset);
      ReefAngel.PWM.Channel3PWMSigmoid(actinicOffset,actinicOffset+lightOffset);
      break;
    }
  }
}

void SetMoon() {
  byte offset=InternalMemory.read(Mem_B_MoonOffset);
 
  byte startD=InternalMemory.read(Mem_B_PWMSlopeStartD);
  byte endD=InternalMemory.read(Mem_B_PWMSlopeEndD);
  byte timeD=InternalMemory.read(Mem_B_PWMSlopeDurationD);

  byte startA=InternalMemory.read(Mem_B_PWMSlopeStartA);
  byte endA=InternalMemory.read(Mem_B_PWMSlopeEndA);
  byte timeA=InternalMemory.read(Mem_B_PWMSlopeDurationA);

  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_PWMSlopeEndD,mp);
    InternalMemory.write(Mem_B_PWMSlopeEndA,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.SetDaylight(0);
  ReefAngel.PWM.SetActinic(0);
 
  switch(InternalMemory.read(Mem_B_MoonMode)) {   
    case 0: {
      // Daylights
      ReefAngel.PWM.SetDaylightRaw(PWMSlopeHighRes(Moon.riseH,Moon.riseM,Moon.setH,Moon.setM,startA,endA,timeA,0));
      ReefAngel.PWM.SetActinicRaw(PWMSlopeHighRes(actRiseH,actRiseM,actSetH,actSetM,startD,endD,timeD,0));
      break;
    }
    case 1: {
      ReefAngel.PWM.SetDaylightRaw(PWMParabolaHighRes(Moon.riseH,Moon.riseM,Moon.setH,Moon.setM, startA,endA,0));
      ReefAngel.PWM.SetActinicRaw(PWMParabolaHighRes(actRiseH,actRiseM,actSetH,actSetM, startD,endD,0));
      break;
    }
  case 2: {
      ReefAngel.PWM.SetDaylightRaw(PWMSmoothRampHighRes(Moon.riseH,Moon.riseM,Moon.setH,Moon.setM,startA,endA,timeA,0));
      ReefAngel.PWM.SetActinicRaw(PWMSmoothRampHighRes(actRiseH,actRiseM,actSetH,actSetM,startD,endD,timeD,0));
      break;
    }
  case 3: {
      ReefAngel.PWM.SetDaylightRaw(PWMSigmoidHighRes(Moon.riseH,Moon.riseM,Moon.setH,Moon.setM,startA,endA,0));
      ReefAngel.PWM.SetActinicRaw(PWMSigmoidHighRes(actRiseH,actRiseM,actSetH,actSetM,startD,endD,0));
      break;
    }
 
  }
}

void FillInMoon() {
  // Extend the sunrise/sunset to fill in gaps when fixtures shut off.
  if(!sun.IsDaytime()) {
    byte actinicOffset=InternalMemory.ActinicOffset_read();
    byte lightOffset=InternalMemory.read(Mem_B_LightOffset); // left right separation
    int LightsOffPerc=40.95*InternalMemory.read(Mem_B_LightsOffPerc);
   
    int onTime1=NumMins(InternalMemory.StdLightsOnHour_read(),InternalMemory.StdLightsOnMinute_read())-(actinicOffset+(2*lightOffset));
    int offTime1=NumMins(InternalMemory.StdLightsOffHour_read(),InternalMemory.StdLightsOffMinute_read())+(actinicOffset+(2*lightOffset));
   
    int onTime2=NumMins(InternalMemory.StdLightsOnHour_read(),InternalMemory.StdLightsOnMinute_read())-(actinicOffset+(2*lightOffset));
    int offTime2=NumMins(InternalMemory.StdLightsOffHour_read(),InternalMemory.StdLightsOffMinute_read())+(actinicOffset+(2*lightOffset));
 
    int moonVal=ReefAngel.PWM.GetDaylightValueRaw();
    int channelVal1=PWMSlopeHighRes(onTime1/60,onTime1%60,offTime1/60,offTime1%60,0,100,lightOffset,0);
    int channelVal2=PWMSlopeHighRes(onTime2/60,onTime2%60,offTime2/60,offTime2%60,0,100,lightOffset,0);
 
    if (channelVal1>ReefAngel.PWM.GetDaylightValueRaw()) ReefAngel.PWM.SetDaylightRaw(channelVal1);
    if (channelVal2>ReefAngel.PWM.GetActinicValueRaw()) ReefAngel.PWM.SetActinicRaw(channelVal2);
     
    DaylightPWMValue=ReefAngel.PWM.GetDaylightValueRaw();
    ActinicPWMValue=ReefAngel.PWM.GetActinicValueRaw();
  }
}

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  Box3_Port1
#define LED_4to1  Box3_Port2
#define LED_3to1  Box3_Port3
#define LED_2to1  Box3_Port4
#define LED_BLUE  Box3_Port5
#define LED_WHITE Box3_Port6
#define LED_MOON  Box3_Port7
#define LED_STORM Box3_Port8

void resetRelayBox(byte ID) {
  // toggle all relays except for the one selected
  for (int i=Box3_Port1;i<=Box3_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);
  DaylightPWMValue=ReefAngel.PWM.GetDaylightValueRaw();
  ActinicPWMValue=ReefAngel.PWM.GetActinicValueRaw();

  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);
    DaylightPWMValue=4095;
    ActinicPWMValue=4095;
    lastPreset=13;
  }

  if (ReefAngel.Relay.isMaskOff(LED_MOON)) {
    if (lastPreset!=14) resetRelayBox(LED_MOON);
    DaylightPWMValue=0;
    ActinicPWMValue=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.SetDaylightRaw(DaylightPWMValue);   
  ReefAngel.PWM.SetActinicRaw(ActinicPWMValue); 

  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);
  }
}

void RFCustom() {
  static boolean changeMode;
  byte rcSpeed, rcSpeedAS;

  // Define new modes
  const int BHazard=15;
  const int RA_ReefCrest=16;
  const int RA_Lagoon=17;
  const int RA_TidalSwell=18;
  const int RA_Smart_NTM=19;
  const int RA_ShortPulse=20;
  const int RA_LongPulse=21;
 
  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;

  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());
}

// 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;
      delta=logLevel[index]-level;
      delta>0?: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);
}

// 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;
       
  level=ReefAngel.WaterLevel.GetLevel();
  if (now()%SECS_PER_DAY-300==20*SECS_PER_HOUR && level<lowLevel) {
      ReefAngel.Relay.Override(VO_RefillATO,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(ROSolenoid,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(ROSolenoid);
      refillAlert.Send("ATO+Refill+is+complete.",true);
    }
  } else {
    if (refillActive) {
      // InternalMemory.write(Mem_B_LogATO, InternalMemory.read(Mem_B_LogATO)+level-prevLevel);
      // InternalMemory.write(Mem_B_LogPrevATO, InternalMemory.read(Mem_B_LogPrevATO)+level-prevLevel);

      if (InternalMemory.read(Mem_B_MaintATO) > 0) // Reset last ATO counter
        InternalMemory.write(Mem_B_MaintATO,0);
      ReefAngel.Relay.Off(ROSolenoid);
      refillActive=false;
    }
  }
 
  // Something in our timing is causing our
  if ((hour()==20 && (minute()>=2 && minute()<=5)) && ReefAngel.Relay.Status(VO_RefillATO)) { ReefAngel.ATOClear(); }
}

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);
  }

  ReefAngel.CustomVar[Var_Feedings]=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;
 
  // 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 RefugiumLight() {
  int runtime=2*SECS_PER_HOUR;
  static boolean trigger=false;
  static time_t t;
 
  // Refugium Light Turned on
  if (ReefAngel.Relay.isMaskOn(Refugium) && trigger==false) {
    t=now();
    trigger=true;
  } 

  if ((now()-t > runtime) && trigger==true) {
    ReefAngel.Relay.Auto(Refugium);
    trigger=false;
  } 
}


// Definitions for dosing pumps
#define numDPumps 3
byte pump[numDPumps]={ DPump1, DPump2, DPump3}; // Pump 3 is just for logging/calibration routine
byte varReport[numDPumps]={ Var_DPump1, Var_DPump2, Var_DPump3};
int memDPTime[numDPumps]={ Mem_B_DP1Timer, Mem_B_DP2Timer, Mem_B_DP3Timer};
int memDPVolume[numDPumps]={ Mem_I_DP1Volume, Mem_I_DP2Volume, Mem_I_DP3Volume };
int memDPRepeat[numDPumps]={ Mem_I_DP1RepeatInterval, Mem_I_DP2RepeatInterval, Mem_I_DP3RepeatInterval };
int memCalTime[numDPumps]={ Mem_I_CalDP1Time, Mem_I_CalDP2Time, Mem_I_CalDP3Time };
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;
    }

//    if (i==2) totalVolume=totalVolume+(InternalMemory.read(Mem_B_VinegarWeek)*4);
//    Instead of this we're going to just add 4ml to the Dosing volume and let this function do nothing different.

    // If any of these are on
    if (ReefAngel.Relay.Status(VO_Calibrate) || ReefAngel.DisplayedMenu==FEEDING_MODE || ReefAngel.DisplayedMenu==WATERCHANGE_MODE) {
      // Do nothing
    } else {
      ReefAngel.DosingPumpRepeat(pump[i], i*5, dpRepeat, doseTime[i]);
    }
  }
}

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_AlkAdjust] > 0 && !ReefAngel.Relay.Status(VO_LockPorts)) {
      alkneeded = alkTarget - ReefAngel.CustomVar[Var_AlkAdjust];
      onTime = 0;
      if (alkneeded > 0 && alkneeded < 10) {
        onTime = 2.295*(float)alkneeded; // onTime is minutes to run, for 70 gallons volume, 1ppm is 2.8ml and at 1.22ml/min, so 2.295 minutes per ppm of alk.
      } else {
        ReefAngel.CustomVar[Var_AlkAdjust] = 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_AlkAdjust] = 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];
  float rate;

  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;
      }
    }

    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
    }
  } 
 
  // See if we are acclimating corals and decrease the countdown each day
  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;
      }
     
    } else {
      // Done with ATO
      ReefAngel.Relay.Off(Swabbie);
      // Clear stale timeout
      ReefAngel.ATOClear();
    }
  } else {
    ReefAngel.Relay.Auto(VO_EnableATO);
    ReefAngel.Relay.Auto(VO_StartFill);   
  }
}

// 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] = Port5Bit | Port6Bit | Port7Bit | Port8Bit;
    ReefAngel.OverridePortsE[1] = Port4Bit | Port5Bit | Port7Bit;   
  } else {
    ReefAngel.OverridePortsE[0] = 0;
    ReefAngel.OverridePortsE[1] = 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;
  char msg[32];   
  char temp[10];
  char ph[10];

  if ((now()+(6*SECS_PER_HOUR))%(12*SECS_PER_HOUR)==0) {
    dtostrf((float)ReefAngel.Params.Temp[T1_PROBE]/10,3, 1, temp);
    dtostrf((float)ReefAngel.Params.PH/100,4, 2, ph);
    sprintf(msg,"Temp:+%s+PH:+%s+WL:+%d%",temp,ph,ReefAngel.WaterLevel.GetLevel());
    dailyReport.Send(msg);
  }
}

void NextRFMode() {
  vtMode++;
 
  if (vtMode > 12) {
    vtMode=0;
    vtSpeed=50; // Constant
  } else if (vtMode == 1) {
    vtSpeed=40; // Lagoon
  } else if (vtMode == 2) {
    vtSpeed=45; // Reef Crest
  } else if (vtMode == 3) { 
    vtSpeed=55; vtDuration=10; // Short Pulse
  } else if (vtMode == 4) {
    vtSpeed=55; vtDuration=20; // Long Pulse
  } else if (vtMode == 5) {
    vtSpeed=InternalMemory.read(Mem_B_NTMSpeed);
    vtDuration=InternalMemory.read(Mem_B_NTMDuration); // Smart_NTM
  } else if (vtMode == 6) {
    vtSpeed=50; vtDuration=10; // Smart_TSM
  } else if (vtMode == 7) {
    vtSpeed=InternalMemory.read(Mem_B_NightSpeed);
    vtDuration=InternalMemory.read(Mem_B_NightDuration);
    vtMode=9; // Night
  } else if (vtMode == 10) {
    vtSpeed=65; vtDuration=5; // Storm
  } else if (vtMode == 11) {
    vtSpeed=45; vtDuration=10; // Custom
  } 

  if (vtMode!=InternalMemory.RFMode_read())
    InternalMemory.RFMode_write(vtMode);
  if (vtSpeed!=InternalMemory.RFSpeed_read())
    InternalMemory.RFSpeed_write(vtSpeed);
  if (vtDuration!=InternalMemory.RFDuration_read())
    InternalMemory.RFDuration_write(vtDuration);
}

// Menu Code
void MenuEntry1() {
  // Toggle refugium light between on/auto.
  ReefAngel.Relay.Override(Refugium, ReefAngel.Relay.Status(Refugium)+1);
  ReefAngel.DisplayedMenu = RETURN_MAIN_MODE;
}
void MenuEntry2() {
  ReefAngel.FeedingModeStart();
}
void MenuEntry3() {
  ReefAngel.WaterChangeModeStart();
}
void MenuEntry4() {
  NextRFMode(); 
  ReefAngel.DisplayedMenu = RETURN_MAIN_MODE;
}
void MenuEntry5() {
  ReefAngel.ATOClear();
  ReefAngel.DisplayMenuEntry("Clear ATO Timeout");
}
void MenuEntry6() {
  ReefAngel.OverheatClear();
  ReefAngel.DisplayMenuEntry("Clear Overheat");
}
void MenuEntry7() {
  ReefAngel.SetupCalibratePH();
}
void MenuEntry8() {
  ReefAngel.SetupCalibrateWaterLevel();
}
void MenuEntry9() {
  ReefAngel.SetupDateTime();
}

// Custom Main Screen
void DrawCustomMain() {
  const int NumScreens=4;
  static boolean drawGraph=true;
 
  // Main Header
  // ReefAngel.LCD.DrawText(DefaultFGColor, DefaultBGColor, 35, 2,"Lee's Reef");
  ReefAngel.LCD.DrawDate(5,2);
  ReefAngel.LCD.Clear(COLOR_BLACK, 1, 11, 128, 11);

  // Param Header
  DrawParams(5,14);
 
  switch (ScreenID) {
    case 0:
    {
      if (drawGraph) { ReefAngel.LCD.DrawGraph(5,40); drawGraph=false; }
      break;
    }
    case 1: { DrawStatus(5,40); break; }
    case 2: { DrawSunMoon(5,40); break; }
    case 3: { DrawClouds(5,50); break; }
  }
 
  // Draw Relays
  DrawRelays(12,94);
 
  // Date+Time
  // ReefAngel.LCD.DrawDate(5,122);
 
  if (ReefAngel.Joystick.IsLeft()) {
    ReefAngel.ClearScreen(DefaultBGColor);
    ScreenID--; drawGraph=true;
  }
  if (ReefAngel.Joystick.IsRight()) {
    ReefAngel.ClearScreen(DefaultBGColor);
    ScreenID++; drawGraph=true;
  }
  if (ScreenID<0) ScreenID=NumScreens-1;
  if (ScreenID>=NumScreens) ScreenID=0;
 
}

void DrawCustomGraph() {
  if (ScreenID==0)
    ReefAngel.LCD.DrawGraph(5, 40);
}

void DrawParams(int x, int y) {
  char buf[16];

  ReefAngel.LCD.DrawText(COLOR_BLACK,DefaultBGColor,x+5,y,"Temp:");
  ReefAngel.LCD.DrawText(COLOR_BLACK,DefaultBGColor,x+80, y, "PH:");
  // Temp and PH
  y+=2;
  ConvertNumToString(buf, ReefAngel.Params.Temp[T2_PROBE], 10);
  ReefAngel.LCD.DrawText(T2TempColor, DefaultBGColor, x+45, y, buf);
  y+=6;
  ConvertNumToString(buf, ReefAngel.Params.Temp[T1_PROBE], 10);
  ReefAngel.LCD.DrawLargeText(T1TempColor, DefaultBGColor, x+5, y, buf, Num8x16);
  ConvertNumToString(buf, ReefAngel.Params.PH, 100);
  ReefAngel.LCD.DrawLargeText(PHColor, DefaultBGColor, x+80, y, buf, Num8x16);
  y+=5;
  ConvertNumToString(buf, ReefAngel.Params.Temp[T3_PROBE], 10);
  ReefAngel.LCD.DrawText(T3TempColor, DefaultBGColor, x+45, y, buf);
}

void DrawStatus(int x, int y) {
  int t=x;
 
  ReefAngel.LCD.DrawLargeText(COLOR_INDIGO,DefaultBGColor,15,y,"High",Font8x16);
  ReefAngel.LCD.DrawLargeText(COLOR_INDIGO,DefaultBGColor,85,y,"Low",Font8x16);
 
  if (ReefAngel.HighATO.IsActive()) {
    ReefAngel.LCD.FillCircle(55,y+3,5,COLOR_GREEN);
  } else {
    ReefAngel.LCD.FillCircle(55,y+3,5,COLOR_RED);
  }
 
  if (ReefAngel.LowATO.IsActive()) {
    ReefAngel.LCD.FillCircle(70,y+3,5,COLOR_GREEN);
  } else {
    ReefAngel.LCD.FillCircle(70,y+3,5,COLOR_RED);
  }
  y+=12;
 
  // Display Water level
  ReefAngel.LCD.DrawText(DefaultFGColor,DefaultBGColor,x,y,"AT0 Level:"); x+=65;
  ReefAngel.LCD.DrawSingleMonitor(ReefAngel.WaterLevel.GetLevel(),DPColor,x,y,1);
  x+=5*(intlength(ReefAngel.WaterLevel.GetLevel())+1);
  ReefAngel.LCD.DrawText(DPColor, DefaultBGColor, x, y, "%");
  y+=12; x=t;
 
  // Vortech Mode
  ReefAngel.LCD.DrawText(0,255,x,y,"RF:"); x+=20;
  ReefAngel.LCD.Clear(DefaultBGColor,x,y,t+(128-t),y+8);
  if (vtMode == 0) ReefAngel.LCD.DrawLargeText(COLOR_GREEN,255,x,y,"Constant");
  else if (vtMode == 1) ReefAngel.LCD.DrawLargeText(COLOR_GOLD,255,x,y,"Lagoon");
  else if (vtMode == 2) ReefAngel.LCD.DrawLargeText(COLOR_GOLD,255,x,y,"Reef Crest");
  else if (vtMode == 3) ReefAngel.LCD.DrawLargeText(COLOR_RED,255,x,y,"Short Pulse");
  else if (vtMode == 4) ReefAngel.LCD.DrawLargeText(COLOR_RED,255,x,y,"Long Pulse");
  else if (vtMode == 5) ReefAngel.LCD.DrawLargeText(COLOR_MAGENTA,255,x,y,"Smart NTM");
  else if (vtMode == 6) ReefAngel.LCD.DrawLargeText(COLOR_MAGENTA,255,x,y,"Tidal Swell");
  else if (vtMode == 9) ReefAngel.LCD.DrawLargeText(COLOR_WHITE,0,x,y,"Night");
  else if (vtMode == 10) ReefAngel.LCD.DrawLargeText(COLOR_BLUE,0,x,y,"Storm");
  else if (vtMode == 11) ReefAngel.LCD.DrawLargeText(COLOR_BLUE,255,x,y,"Custom");
  y+=10; x=t;
 
  ReefAngel.LCD.DrawText(0,255,x,y,"RF Speed:"); x+=60;
  ReefAngel.LCD.Clear(DefaultBGColor,x,y,128,y+8);
  ReefAngel.LCD.DrawText(COLOR_BLUE, DefaultBGColor,x,y,vtSpeed); x+=15;
  ReefAngel.LCD.DrawText(COLOR_BLUE, DefaultBGColor,x,y,"/"); x+=10;
  ReefAngel.LCD.DrawText(COLOR_BLUE, DefaultBGColor,x,y,vtDuration);
  y+=10; x=t;
 
  // Display Acclimation timer
  byte acclDay=InternalMemory.read(Mem_B_AcclDay);
 
  if (acclDay > 0) {
    ReefAngel.LCD.DrawText(DefaultFGColor,DefaultBGColor,x,y,"Acclimation Day:"); x+=100;
    ReefAngel.LCD.DrawSingleMonitor(acclDay,DefaultFGColor,x,y,1);
  } else {
    ReefAngel.LCD.Clear(DefaultBGColor,x,y,128,y+8);
  }
}

void DrawSunMoon(int x, int y) {
  char buf[16];
  int t=x;

  y+=2;
  /// Display Sunrise / Sunset
  sprintf(buf, "%02d:%02d", sun.GetRiseHour(), sun.GetRiseMinute());
  ReefAngel.LCD.DrawText(COLOR_BLACK,DefaultBGColor,x,y,"Rise:"); x+=31;
  ReefAngel.LCD.DrawText(COLOR_RED,DefaultBGColor,x,y,buf);
  sprintf(buf, "%02d:%02d", sun.GetSetHour(), sun.GetSetMinute()); x+=36;
  ReefAngel.LCD.DrawText(COLOR_BLACK,DefaultBGColor,x,y,"Set:"); x+=25;
  ReefAngel.LCD.DrawText(COLOR_RED,DefaultBGColor,x,y,buf);
  y+=15; x=t;
 
  /// Display Moonrise / Moonset
  sprintf(buf, "%02d:%02d", Moon.riseH, Moon.riseM);
  ReefAngel.LCD.DrawText(COLOR_BLACK,DefaultBGColor,x,y,"MR:"); x+=21;
  ReefAngel.LCD.DrawText(COLOR_RED,DefaultBGColor,x,y,buf);
  sprintf(buf, "%02d:%02d", Moon.setH, Moon.setM); x+=36;
  ReefAngel.LCD.DrawText(COLOR_BLACK,DefaultBGColor,x,y,"MS:"); x+=21;
  ReefAngel.LCD.DrawText(COLOR_RED,DefaultBGColor,x,y,buf); x+=36;
  if (Moon.isUp) ReefAngel.LCD.DrawText(COLOR_RED,DefaultBGColor,x,y,"@");
    else ReefAngel.LCD.DrawText(COLOR_RED,DefaultBGColor,x,y,"_");
  y+=10; x=t;
 
  // MoonPhase
  ReefAngel.LCD.DrawText(0,255,x,y,"Moon:");
  ReefAngel.LCD.Clear(DefaultBGColor,x+32,y,128,y+8);
  ReefAngel.LCD.DrawText(COLOR_MAGENTA,255,x+32,y,MoonPhaseLabel());
  y+=10; x=t;
 
  // MoonLight %
  ReefAngel.LCD.DrawText(COLOR_BLACK,DefaultBGColor,x,y,"MoonLights:"); x+=68;
  ReefAngel.LCD.DrawSingleMonitor(ReefAngel.PWM.GetDaylightValue(),DPColor,x,y,1);
  x+=5*(intlength(ReefAngel.PWM.GetDaylightValue())+1);
  ReefAngel.LCD.DrawText(DPColor, DefaultBGColor, x, y, "%");
}

void DrawRelays(int x, int y) {
  // Draw Relays
  byte TempRelay = ReefAngel.Relay.RelayData;
  TempRelay &= ReefAngel.Relay.RelayMaskOff;
  TempRelay |= ReefAngel.Relay.RelayMaskOn;
  ReefAngel.LCD.DrawOutletBox(x, y, TempRelay);

  y+=12;
  TempRelay = ReefAngel.Relay.RelayDataE[0];
  TempRelay &= ReefAngel.Relay.RelayMaskOffE[0];
  TempRelay |= ReefAngel.Relay.RelayMaskOnE[0];
  ReefAngel.LCD.DrawOutletBox(x, y, TempRelay);
 
  y+=12;
  TempRelay = ReefAngel.Relay.RelayDataE[1];
  TempRelay &= ReefAngel.Relay.RelayMaskOffE[1];
  TempRelay |= ReefAngel.Relay.RelayMaskOnE[1];
  ReefAngel.LCD.DrawOutletBox(x, y, TempRelay); 
}

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<14) {
        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 DrawClouds(int x, int y)
{
    // Write the times of the next cloud, next lightning, and cloud duration to the screen and into some customvars for the Portal.
    ReefAngel.LCD.DrawText(0,255,x,y,"C"); x+=6;
    ReefAngel.LCD.DrawText(0,255,x,y,"00:00"); x+=34;
    ReefAngel.LCD.DrawText(0,255,x,y,"L"); x+=6;
    ReefAngel.LCD.DrawText(0,255,x,y,"00:00"); x=5;
    if (cloudchance && (NumMins(hour(),minute())<cloudstart))
    {
      int x=0;
      if ((cloudstart/60)>=10) x=11;
      else x=17;
      ReefAngel.LCD.DrawText(0,255,x,y,(cloudstart/60));
      //ReefAngel.CustomVar[3]=cloudstart/60; // Write the hour of the next cloud to custom variable for Portal reporting
      if ((cloudstart%60)>=10) x=29;
      else x=35;
      ReefAngel.LCD.DrawText(0,255,x,y,(cloudstart%60));
      //ReefAngel.CustomVar[4]=cloudstart%60; // Write the minute of the next cloud to custom variable for Portal reporting

    }
    ReefAngel.LCD.DrawText(0,255,x+85,y,cloudduration);
    //ReefAngel.CustomVar[7]=(cloudduration);    // Put the duration of the next cloud in a custom var for the portal
    if (lightningchance)
    {
      int x=0;
      if (((cloudstart+(cloudduration/3))/60)>=10) x=51;
      else x=57;
      ReefAngel.LCD.DrawText(0,255,x,y,((cloudstart+(cloudduration/3))/60));
      //ReefAngel.CustomVar[5]=(cloudstart+(cloudduration/2))/60;    // Write the hour of the next lightning to a custom variable for the Portal
      if (((cloudstart+(cloudduration/3))%60)>=10) x=69;
      else x=75;
        ReefAngel.LCD.DrawText(0,255,x,y,((cloudstart+(cloudduration/3))%60)); // Write the minute of the next lightning to a custom variable for the Portal
        //ReefAngel.CustomVar[6]=(cloudstart+(cloudduration/2))%60;
    }
}

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: 5356
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: 5356
Joined: Fri Jul 20, 2012 9:42 am
PostPosted: Fri Dec 28, 2012 10:03 pm
ImageImageImage
User avatar
Posts: 2745
Joined: Fri Mar 18, 2011 6:20 pm
Location: Illinois
PostPosted: Sat Dec 29, 2012 8:27 am
Awesome! Great write up.

Posts: 12282
Joined: Fri Mar 18, 2011 6:47 pm
PostPosted: Sun Dec 30, 2012 10:25 am
Very cool!!! 8-)
Roberto.
User avatar
Posts: 5356
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: 12282
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: 5356
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: 12282
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 0 guests