Lee's Feature Complete PDE

Share you PDE file with our community
Post Reply
User avatar
lnevo
Posts: 5430
Joined: Fri Jul 20, 2012 9:42 am

Lee's Feature Complete PDE

Post by lnevo »

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
lnevo
Posts: 5430
Joined: Fri Jul 20, 2012 9:42 am

Re: Lee's Feature Complete PDE

Post by lnevo »

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
lnevo
Posts: 5430
Joined: Fri Jul 20, 2012 9:42 am

Re: Lee's Feature Complete PDE

Post by lnevo »

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
lnevo
Posts: 5430
Joined: Fri Jul 20, 2012 9:42 am

Re: Lee's Feature Complete PDE

Post by lnevo »

ImageImageImage
binder
Posts: 2871
Joined: Fri Mar 18, 2011 6:20 pm
Location: Illinois
Contact:

Re: Lee's Feature Complete PDE

Post by binder »

Awesome! Great write up.
rimai
Posts: 12881
Joined: Fri Mar 18, 2011 6:47 pm

Re: Lee's Feature Complete PDE

Post by rimai »

Very cool!!! 8-)
Roberto.
User avatar
lnevo
Posts: 5430
Joined: Fri Jul 20, 2012 9:42 am

Re: Lee's Feature Complete PDE

Post by lnevo »

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%!!! :)
rimai
Posts: 12881
Joined: Fri Mar 18, 2011 6:47 pm

Re: Lee's Feature Complete PDE

Post by rimai »

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
lnevo
Posts: 5430
Joined: Fri Jul 20, 2012 9:42 am

Re: Lee's Feature Complete PDE

Post by lnevo »

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...
rimai
Posts: 12881
Joined: Fri Mar 18, 2011 6:47 pm

Re: Lee's Feature Complete PDE

Post by rimai »

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.
User avatar
lnevo
Posts: 5430
Joined: Fri Jul 20, 2012 9:42 am

Re: Lee's Feature Complete PDE

Post by lnevo »

Added in the PWMSlopeOvernight() function so that now my moonlights will dim off and then dim back on (Well actually they are going the opposite way, but "overnight" so it just seems that way :D )

Also simplified my isNight check using the new GetSunRise() and GetSunSet() functions added to the SunLocation class. Much easier now since we can compare to now().

Finally, added a function (RefugiumLights()) similar to MoonLights() that will respect the ActinicOffset() and turn on my refugium light opposite my actinics based on the StandardLights memory variables.
rimai
Posts: 12881
Joined: Fri Mar 18, 2011 6:47 pm

Re: Lee's Feature Complete PDE

Post by rimai »

Cool!!
Roberto.
TanksNStuff
Posts: 188
Joined: Fri Dec 30, 2011 6:57 am

Re: Lee's Feature Complete PDE

Post by TanksNStuff »

Lee, was just reviewing your code from your 2nd post in this thread. I like a lot of the features you included and I will likely try to adopt a few of them for my custom code.

However, I was curious/confused as to why you put all this at the end when typically the custom menu and display stuff is usually at the beginning:

Code: Select all

  ////// Place your custom code above here

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

// Vortech Helper functions
void setRFmode(int mode, int speed, int duration) {

  // Check if mode has changed
  if (mode!=vtMode) {  
    vtPrevMode=vtMode;
    vtMode=mode; 
  
    if (mode!=InternalMemory.RFMode_read()) { 
      InternalMemory.RFMode_write(mode); 
    }
    
    // Fix for coming out of night mode
    if (vtPrevMode==Night) {
      ReefAngel.RF.UseMemory=false;
      ReefAngel.RF.SetMode(Feeding_Stop,0,0);
      ReefAngel.RF.UseMemory=true;
    }
    
    // If it's at night or we are setting NTM, do this only temporarily
    if ( (isNight && vtMode != Night) || vtMode==Smart_NTM) {
      setRFtimer(60);
    }
  } 

  // Check if speed has changed
  if (speed!=vtSpeed) {  
    vtPrevSpeed=vtSpeed;
    vtSpeed=speed;
    
    if (speed!=InternalMemory.RFSpeed_read()) {
      InternalMemory.RFSpeed_write(speed);
    } 
  }

  // Check if duration has changed
  if (duration!=vtDuration) {  
    vtPrevDuration=vtDuration;
    vtDuration=duration; 
    
    if (speed!=InternalMemory.RFSpeed_read()) {
      InternalMemory.RFSpeed_write(speed);
    }
  }
}
void setRFmode() {
  setRFmode(InternalMemory.RFMode_read(), InternalMemory.RFSpeed_read(), InternalMemory.RFDuration_read());
}
void setRFtimer(int minutes) {
  ReefAngel.Timer[1].SetInterval(minutes*60);
  ReefAngel.Timer[1].Start();
  vtOverride=true;
}

byte PWMSlopeOvernight(byte startHour, byte startMinute, byte endHour, byte
endMinute, byte startPWM, byte endPWM, byte Duration, byte oldValue)
{
  
  unsigned long Start = previousMidnight(now())+((unsigned long)NumMins(startHour, startMinute)*60);
  if (hour()<startHour) Start-=86400;
  unsigned long StartD = Start + (Duration*60);
  unsigned long End = nextMidnight(now())+((unsigned long)NumMins(endHour, endMinute)*60);
  if (hour()<startHour) End-=86400;
  unsigned long StopD = End - (Duration*60);
  if ( now() >= Start && now() <= StartD )
    return constrain(map(now(), Start, StartD, startPWM, endPWM),startPWM,
    endPWM);
  else if ( now() >= StopD && now() <= End )
  {
    byte v = constrain(map(now(), StopD, End, startPWM, endPWM),startPWM,
    endPWM);
    return endPWM-v+startPWM;
  }
  else if ( now() > StartD && now() < StopD )
    return endPWM;

  // lastly return the existing value
  return oldValue;
}

// Similar to MoonLights() but adding in ActinicOffset
void RefugiumLights(byte Relay)
{
  int MinuteOffset=InternalMemory.ActinicOffset_read();
  int onTime=NumMins(InternalMemory.StdLightsOffHour_read(),InternalMemory.StdLightsOffMinute_read())+MinuteOffset;
  int offTime=NumMins(InternalMemory.StdLightsOnHour_read(),InternalMemory.StdLightsOnMinute_read())-MinuteOffset;
  
  ReefAngel.StandardLights(Relay,onTime/60,onTime%60,offTime/60,offTime%60);
}

// Menu Code
void MenuEntry1() {
  ReefAngel.FeedingModeStart();
}
void MenuEntry2() {
  ReefAngel.WaterChangeModeStart();
}
void MenuEntry3() {
  byte mode,speed,duration;
  byte prev_mode,prev_speed,prev_dur;
  
  mode=vtMode;
  mode++;
  
  prev_mode=vtPrevMode; prev_speed=vtPrevSpeed; prev_dur=vtPrevDuration;
  
  if (mode > 9) { 
    mode=0; 
    speed=50; duration=0; // Constant
  } else if (mode == 1) { 
    speed=40; duration=0; // Lagoon
  } else if (mode == 2) { 
    speed=45; duration=0; // Reef Crest
  } else if (mode == 3) {  
    speed=55; duration=10; // Short Pulse
  } else if (mode == 4) {
    speed=55; duration=20; // Long Pulse
  } else if (mode == 6) {
    speed=50; duration=10; // Smart_TSM
  } else if (mode == 5) {
    speed=vtNTMSpeed; duration=vtNTMDuration; // Smart_NTM
  } else if (mode == 7) {
    speed=vtNightSpeed; duration=vtNightDuration; // Night
    mode=9; 
  }  

  // Backup the previous modes. We don't want Night to become default...
  prev_mode=vtPrevMode; prev_speed=vtPrevSpeed; prev_dur=vtPrevDuration;
  setRFmode(mode,speed,duration);
  
  // If it's night time, don't overwrite the default daytime mode when using the menu
  if (!isNight) {
    vtPrevMode=prev_mode; vtPrevSpeed=prev_speed; vtPrevDuration=prev_dur;
  }
  
  ReefAngel.DisplayedMenu = RETURN_MAIN_MODE;   
}
void MenuEntry4() {
  // Toggle the refugium light if we choose this menu entry.
  // Behavior is opposite for night vs day.
  
  if (isNight) { // Turn off the Refugium light
    if (bitRead(ReefAngel.Relay.RelayMaskOff,Refugium-1)==1) {
      bitClear(ReefAngel.Relay.RelayMaskOff,Refugium-1);
    } else {
      bitSet(ReefAngel.Relay.RelayMaskOff,Refugium-1);
    }
  } else { // Turn on the Refugium light
    if (bitRead(ReefAngel.Relay.RelayMaskOn,Refugium-1)==1) {
      bitClear(ReefAngel.Relay.RelayMaskOn,Refugium-1);
    } else {
      bitSet(ReefAngel.Relay.RelayMaskOn,Refugium-1);
    }
  }
  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();
  ReefAngel.DisplayedMenu = ALT_SCREEN_MODE;
}
void MenuEntry8() {
  ReefAngel.SetupCalibrateWaterLevel();
  ReefAngel.DisplayedMenu = ALT_SCREEN_MODE;
}
void MenuEntry9() {
  ReefAngel.SetupDateTime();
  ReefAngel.DisplayedMenu = ALT_SCREEN_MODE;
}

// Custom Main Screen
void DrawCustomMain() {
  char buf[16];
  byte x = 5;
  byte y = 2;
  byte t;

  // Main Header
  // ReefAngel.LCD.DrawText(DefaultFGColor, DefaultBGColor, 35, y,"Lee's Reef");
  // Had no room for this anymore :(
  
  // Date+Time
  ReefAngel.LCD.DrawDate(x+1, y);
  ReefAngel.LCD.Clear(COLOR_BLACK, 1, y+9, 128, y+9);
  
  // Param Header
  y+=12; 
  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);
  pingSerial();
    
  /// Display Sunrise / Sunset (to be calculated later...)
  y+=12; t=x;
  sprintf(buf, "%02d:%02d", sl.GetRiseHour(), sl.GetRiseMinute());
  ReefAngel.LCD.DrawText(COLOR_BLACK,DefaultBGColor,t,y,"Rise:"); t+=31;
  ReefAngel.LCD.DrawText(COLOR_RED,DefaultBGColor,t,y,buf); 
  sprintf(buf, "%02d:%02d", sl.GetSetHour(), sl.GetSetMinute()); t+=36;
  ReefAngel.LCD.DrawText(COLOR_BLACK,DefaultBGColor,t,y,"Set:"); t+=25;
  ReefAngel.LCD.DrawText(COLOR_RED,DefaultBGColor,t,y,buf);
  pingSerial();

  // MoonPhase
  y+=10; 
  ReefAngel.LCD.DrawText(0,255,x,y,"Moon:");
  ReefAngel.LCD.Clear(DefaultBGColor,x+32,y,x+(128-x),y+8);
  ReefAngel.LCD.DrawText(COLOR_MAGENTA,255,x+32,y,MoonPhaseLabel());
  pingSerial();
  
  // MoonLight %
  y+=10;
  t = intlength(ReefAngel.PWM.GetDaylightValue()) + 1;  t *= 5;
  ReefAngel.LCD.DrawText(COLOR_BLACK,DefaultBGColor,x,y,"MoonLights:"); 
  ReefAngel.LCD.DrawSingleMonitor(ReefAngel.PWM.GetDaylightValue(), DPColor, x+68, y, 1);
  ReefAngel.LCD.DrawText(DPColor, DefaultBGColor, x+68+t, y, "%");
  pingSerial();

  // Display Water level
  y+=10; t=x;
  ConvertNumToString(buf, ReefAngel.WaterLevel.GetLevel(), 1);
  strcat(buf,"  ");
  ReefAngel.LCD.DrawText(DefaultFGColor,DefaultBGColor,x,y,"AT0 Level:"); t+=60;
  ReefAngel.LCD.DrawText(DefaultFGColor,DefaultBGColor,t,y,buf);

  // Vortech Mode
  y+=10; t=x;
  ReefAngel.LCD.DrawText(0,255,x,y,"RF:"); t+=20;
  ReefAngel.LCD.Clear(DefaultBGColor,t,y,x+(128-x),y+8);
  if (vtMode == 0) ReefAngel.LCD.DrawLargeText(COLOR_GREEN,255,t,y,"Constant");
  else if(vtMode == 1) ReefAngel.LCD.DrawLargeText(COLOR_GOLD,255,t,y,"Lagoon");
  else if (vtMode == 2) ReefAngel.LCD.DrawLargeText(COLOR_GOLD,255,t,y,"Reef Crest");
  else if (vtMode == 3) ReefAngel.LCD.DrawLargeText(COLOR_RED,255,t,y,"Short Pulse");
  else if (vtMode == 4) ReefAngel.LCD.DrawLargeText(COLOR_RED,255,t,y,"Long Pulse");
  else if (vtMode == 5) ReefAngel.LCD.DrawLargeText(COLOR_MAGENTA,255,t,y,"Smart NTM");
  else if (vtMode == 6) ReefAngel.LCD.DrawLargeText(COLOR_MAGENTA,255,t,y,"Tidal Swell");
  else if (vtMode == 9) ReefAngel.LCD.DrawLargeText(COLOR_WHITE,0,t,y,"Night");
  y+=10; t=x;
  ReefAngel.LCD.DrawText(0,255,x,y,"RF Speed:"); t+=60;
  ReefAngel.LCD.Clear(DefaultBGColor,t,y,x+(128-x),y+8);
  ReefAngel.LCD.DrawText(COLOR_BLUE, DefaultBGColor,t,y,InternalMemory.RFSpeed_read()); t+=15;
  ReefAngel.LCD.DrawText(COLOR_BLUE, DefaultBGColor,t,y,"/"); t+=10;
  ReefAngel.LCD.DrawText(COLOR_BLUE, DefaultBGColor,t,y,InternalMemory.RFDuration_read());
  pingSerial();
  
  // Display Water level
  y+=10; t=x;
  if (acclDay > 0) {
    ConvertNumToString(buf, acclDay, 1);
    strcat(buf,"  ");
    ReefAngel.LCD.DrawText(DefaultFGColor,DefaultBGColor,x,y,"Acclimation Day:"); t+=100;
    ReefAngel.LCD.DrawText(DefaultFGColor,DefaultBGColor,t,y,buf);
  } else {
    ReefAngel.LCD.Clear(DefaultBGColor,x,y,x+(128-x),y+8);
  }
  
  // Relays
  y+=10; t=x+7;
  byte TempRelay = ReefAngel.Relay.RelayData;
  TempRelay &= ReefAngel.Relay.RelayMaskOff;
  TempRelay |= ReefAngel.Relay.RelayMaskOn;
  ReefAngel.LCD.DrawOutletBox(t, y, TempRelay);
  pingSerial();
  y+=12;
  TempRelay = ReefAngel.Relay.RelayDataE[0];
  TempRelay &= ReefAngel.Relay.RelayMaskOffE[0];
  TempRelay |= ReefAngel.Relay.RelayMaskOnE[0];
  ReefAngel.LCD.DrawOutletBox(t, y, TempRelay);
  pingSerial();
}

void DrawCustomGraph() {
}

Was it necessary to put it there for some reason? You didn't follow the " // This should always be the last line
" comments.
User avatar
lnevo
Posts: 5430
Joined: Fri Jul 20, 2012 9:42 am

Re: Lee's Feature Complete PDE

Post by lnevo »

TanksNStuff wrote:

Code: Select all

  ////// Place your custom code above here

  // This should always be the last line
  ReefAngel.Portal("lnevo");
  ReefAngel.ShowInterface();
}
Was it necessary to put it there for some reason? You didn't follow the " // This should always be the last line
" comments.
If you look at the function loop() I did follow the " // This should always be the last line" comment. That comment means those two calls should be the last things that are executed by the loop() function.

The reason the rest of the stuff is below it was just a means to make it easier for me to edit and update the code. I was tried of scrolling through all the menu code and helper functions. This way, after my variable declarations and such, I would just have setup() followed by loop() where most of my code is located. It's really just a matter of preference :)
TanksNStuff
Posts: 188
Joined: Fri Dec 30, 2011 6:57 am

Re: Lee's Feature Complete PDE

Post by TanksNStuff »

Oh, OK. Making it more convenient is a good excuse. I just thought that all of that had to be up top. I didn't realize it would still work if it was after the loop.
User avatar
lnevo
Posts: 5430
Joined: Fri Jul 20, 2012 9:42 am

Re: Lee's Feature Complete PDE

Post by lnevo »

A little bit of cleanup today. I removed the reliance on my isNight boolean variable.... I didn't think it was really needed.

In doing so, I streamlined my toggle for the Refugium light from the menu.

Code: Select all

  if (bitRead(ReefAngel.Relay.RelayData, Refugium-1)) { // If relay is on.
    // Toggle MaskOff for the light
    bitWrite(ReefAngel.Relay.RelayMaskOff, Refugium-1, 1-bitRead(ReefAngel.Relay.RelayMaskOff, Refugium-1));
  } else { 
    // Toggle the MaskOn for the light
    bitWrite(ReefAngel.Relay.RelayMaskOn, Refugium-1, 1-bitRead(ReefAngel.Relay.RelayMaskOn, Refugium-1));
  }
I also streamlined some of my RF menu functions, so if I change from the menu, it's always a temporary change. I only set the timer when I go to Smart_NTM now or if done from the menu. Changing the memory setting is now the only way to change the default RF mode. If it's changed at night, it should only change momentarily (unless Smart_NTM which will trigger the timer..)

I also came up with a neat trick to have something run at a particular time of day. Instead of doing comparing now() to hour(), minute(), and second()...

Code: Select all

  if (now()%SECS_PER_DAY==54000) { // 3pm.
    setRFmode(Smart_NTM,vtNTMSpeed,vtNTMDuration);
  }
binder
Posts: 2871
Joined: Fri Mar 18, 2011 6:20 pm
Location: Illinois
Contact:

Re: Lee's Feature Complete PDE

Post by binder »

TanksNStuff wrote:Oh, OK. Making it more convenient is a good excuse. I just thought that all of that had to be up top. I didn't realize it would still work if it was after the loop.
Technically it shouldn't work at the end of the file unless you define the functions up top before they are used (standard C/C++ syntax). However, arduino does things a little different because what we write in our PDE/INO files is actually included into a "base" main file that runs our setup() function first and then falls into an infinite loop calling the loop() function. Plus the arduino preprocessor appears to scan and generate other files needed to make things work. So the functions at the bottom of our file are already defined before they are used.
Yeah, it's a little confusing and it took me a while to understand what it was doing too. I looked at this when I was considering adding in compilation to my old RAGen program.
User avatar
lnevo
Posts: 5430
Joined: Fri Jul 20, 2012 9:42 am

Re: Lee's Feature Complete PDE

Post by lnevo »

Hey it works... :) I moved around some of the code actually because my helper functions were starting to take more room and getting in the way between my loop() and Menu functions which I edit more often.

Also, played around with different GPS coordinates. Ended up off the coast of Chile to get a smaller timezone offset, but maintaining the day length and season cycle of GBR. I may switch in the spring to North of the equator to get in sync with our Winter/Summer day length schedule.. we'll see how this shapes up...
User avatar
lnevo
Posts: 5430
Joined: Fri Jul 20, 2012 9:42 am

Re: Lee's Feature Complete PDE

Post by lnevo »

So this week I did quite a bit of work on my code. I hacked up a routine to automate my water change and finished up the Tide class code. I had some time to test the water change code this weekend and it did take some tweaking because my float switch I'm trying to use is upside down for the SingleATO function to work properly. Currently I hacked the SingleATO function :)

Anyway, when I get my skimmate collector, I will rewire and re-orient my float switches. I was going to put both switches in parallel but since I need the float switch in active mode to trigger the ATO functions, I'm going to put them in series and have them both activated. If either goes in-active it will break the circuit and disable my return pump. When in water change mode though, it will turn my WC pump into an ATO pump :)

I also finished up the design for my Night mode since with my Tide program, I didn't want to interrupt the high tide/low tide functionality. I used the PWMSlope to create a transition between Night Speed and my normal speed. The value will be used to adjust the Tide class speed.

Anyway, code will be posted up soon. Looking forward to testing the tide stuff and getting to do more frequent water changes. I'm really pleased with my method for water changing now, just need to fine tune the plumbing and the process. I really like being able to rinse new media while simultaneously doing a water change :)
User avatar
lnevo
Posts: 5430
Joined: Fri Jul 20, 2012 9:42 am

Re: Lee's Feature Complete PDE

Post by lnevo »

Ok, I'm archiving my last version before I moved forward with my non-memory based RF code if anyone needs to reference it. The new version still uses memory but since the speed is going to change constantly based on my new Custom mode, I didn't want to keep reading/writing from memory. So here you go if anyone needs the reference for some reason.

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 <RF.h>
#include <ReefAngel.h>
#include <SunLocation.h>
#include <WaterLevel.h>

#define NUMBERS_8x16

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

// Custom Menu Code
#include <avr/pgmspace.h>
prog_char menu1_label[] PROGMEM = "Feeding";
prog_char menu2_label[] PROGMEM = "Water Change";
prog_char menu3_label[] PROGMEM = "Vortech Mode";
prog_char menu4_label[] PROGMEM = "Refugium Light";
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
};

// Vortech Defaults
byte vtPrevMode=0;
byte vtPrevSpeed=0;
byte vtPrevDuration=0;
// Default Mode
byte vtMode=Random2;
byte vtSpeed=45;
byte vtDuration=5;
// NTM mode
byte vtNTMSpeed=65;
byte vtNTMDuration=5;
// Night Mode
byte vtNightSpeed=20;
byte vtNightDuration=10;
TimerClass rfTimer;

boolean isFeeding=false;
boolean feedDelay=false;
boolean vtOverride=false;
boolean floatHigh=true;
boolean powerOutage=true;

SunLocation sl;
byte acclDay=0;
byte vacationMode=0;

byte wcReady=0;
int wcFillTime=0;
TimerClass wcTimer;

//Define Custom Memory Location
#define Mem_B_RefillATO   100
#define Mem_B_Vacation    102
#define Mem_B_AcclDay     103
#define Mem_B_WaterChange 105
#define Mem_I_WCFillTime  106

#define Var_HighATO    0
#define Var_Power      1
#define Var_Vacation   2
#define Var_AcclDay    3

//Define Relay Ports by Name
#define Return             1
#define Skimmer            2
#define WhiteLEDs          3
#define BlueLEDs           4
#define Extension          5
#define Heater             6
#define Refugium           7
#define Reactor            8

#define Unused1            Box1_Port1
#define Unused2            Box1_Port2
#define Vortech1           Box1_Port3
#define Vortech2           Box1_Port4
#define VortechUPS         Box1_Port5
#define Unused3            Box1_Port6
#define DPump1             Box1_Port7
#define DPump2             Box1_Port8

////// Place global variable code above here

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

    // 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 = Port3Bit | Port4Bit | Port6Bit;
    // Use T1 probe as temperature and overheat functions
    ReefAngel.TempProbe = T1_PROBE;
    ReefAngel.OverheatProbe = T1_PROBE;
    
    // Ports that default on
    ReefAngel.Relay.On(Return);
    ReefAngel.Relay.On(Vortech1);
    ReefAngel.Relay.On(Vortech2);
    ReefAngel.Relay.On(VortechUPS);
    // Ports that default off
    ReefAngel.Relay.Off(Extension);
    ReefAngel.Relay.Off(Unused1);
    ReefAngel.Relay.Off(Unused2);
    ReefAngel.Relay.Off(Unused3);
    
    ////// Place additional initialization code below here
    // sl.Init(-21.08931,-147.699722); // Default - GBR
    // sl.Init(21.30891,-147.699722); // Near Honolulu, HI
    sl.Init(-21.08931, -73.528383); // Off the coast of Chile
    
    // What was our previous modes before we restarted?
    vtPrevMode=InternalMemory.RFMode_read();
    vtPrevSpeed=InternalMemory.RFSpeed_read();
    vtPrevDuration=InternalMemory.RFDuration_read();
    
    // Dummy CustomVar to activate portal feature
    ReefAngel.CustomVar[7]=255;
    
    ////// Place additional initialization code above here
}

void loop()
{
  // Default port modes. Use Memory settings for external control
  ReefAngel.StandardHeater(Heater);
  //ReefAngel.DosingPumpRepeat1(DPump1);
  //ReefAngel.DosingPumpRepeat2(DPump2);
  ReefAngel.StandardLights(WhiteLEDs);
  ReefAngel.ActinicLights(BlueLEDs);
    
  ////// Place your custom code below here

  RefugiumLights(Refugium);
  
  // Moonlights on from 5:00am-3:00am so 3am-5am is complete darkness 
  ReefAngel.PWM.SetDaylight(PWMSlopeOvernight(5,0,3,0,0,MoonPhase(),30,0));
  ReefAngel.PWM.SetActinic(PWMSlopeOvernight(5,0,3,0,0,MoonPhase(),30,0));
  
  // See if power is back on so DelayedOn ports will reset.
  if (powerOutage && ReefAngel.Relay.IsRelayPresent(EXP1_RELAY))
  {
    powerOutage=false;
    LastStart=now();
    ReefAngel.CustomVar[Var_Power]=0;
  }
  ReefAngel.Relay.DelayedOn(Skimmer); 
  ReefAngel.Relay.DelayedOn(Reactor,1); 

  // See if we are acclimating corals and decrement the countdown each night
  static boolean acclCounterReady=false;
  if (now()%SECS_PER_DAY!=0) acclCounterReady=true;
  
  acclDay=InternalMemory.read(Mem_B_AcclDay);
  ReefAngel.CustomVar[Var_AcclDay]=acclDay;
  if (acclDay > 0) { 
    if (acclCounterReady && now()%SECS_PER_DAY==0) {
      acclDay--;
      acclCounterReady=false;
      InternalMemory.write(Mem_B_AcclDay,acclDay);
    }
  }  
  
  // -9 hour difference for time zone. 472/506 seconds were calculation corrections
  // The acclDay will adjust the sunrise/sunset if we are adjusting for new coral
  // sl.SetOffset(14,472+(acclDay*240),14,506-(acclDay*120)); // GBR
  // sl.SetOffset(-6,472(acclDay*240),-6,506-(acclDay*120)); // Honolulu, HI
  sl.SetOffset(-1,(acclDay*240),-1,(-acclDay*120)); // Off the coast of Chile
  
  // Calculate the new Sunrise / Sunset based on our GPS coordinates
  sl.CheckAndUpdate();

  if ( (now() >= sl.GetSunRise()) && (now() <= (sl.GetSunSet()-30)) ) // It's Daytime
  {
    // Turn off MoonLights
    ReefAngel.PWM.SetDaylight(0);
    ReefAngel.PWM.SetActinic(0);

    // Come out of Night mode.
    if (vtOverride==false && vtMode==Night) {
      setRFmode(vtPrevMode,vtPrevSpeed,vtPrevDuration);
    }
  } else { 
    // Set Vortech's to Night mode.
    if (vtOverride==false && isFeeding==false) {
      setRFmode(Night,vtNightSpeed,vtNightDuration);
    }
  }   
 
  // Some Tidal Swell 
  if (now()%SECS_PER_DAY==43200) { // 12pm.
    setRFmode(Smart_TSM,50,10);
    setRFtimer(60);
  }  
  
  // Some Short Pulse action
  if (now()%SECS_PER_DAY==50400) { // 2pm.
    setRFmode(ShortPulse,55,10);
    setRFtimer(30);
  }  
  
  // A little extra Smart_NTM never hurt anyone
  if (now()%SECS_PER_DAY==54000) { // 3pm.
    setRFmode(Smart_NTM,vtNTMSpeed,vtNTMDuration);
  }
    
  // Some lagoon action
  if (now()%SECS_PER_DAY==59400) { // 4:30pm.
    setRFmode(Lagoon,40,0);
    setRFtimer(30);
  }
  
  // Some Long Pulse action
  if (now()%SECS_PER_DAY==64800) { // 6pm
    setRFmode(LongPulse,55,20);
    setRFtimer(30);
  }

  // Enable Feeding Mode flag
  if (ReefAngel.DisplayedMenu==FEEDING_MODE) isFeeding=true;
  // Turn on Refugium light during feeding anwater change mode
  if (ReefAngel.DisplayedMenu==FEEDING_MODE || ReefAngel.DisplayedMenu==WATERCHANGE_MODE) ReefAngel.Relay.On(Refugium);
  // Enable ATOHigh flag on purpose during feed/water change mode so we don't get alerts.
  if (ReefAngel.DisplayedMenu==FEEDING_MODE || ReefAngel.DisplayedMenu==WATERCHANGE_MODE) floatHigh=true;
    
  // Here's what we do if we're just out of feeding mode...
  if (ReefAngel.DisplayedMenu==DEFAULT_MENU && isFeeding) {
    isFeeding=false; 
    feedDelay=true; // This will let us know we want some extra time before Smart_NTM
    setRFtimer(30); // Start Smart_NTM in 30 minutes...
  } else if (vtOverride && rfTimer.IsTriggered()) { // Our RF timer is over. 
    vtOverride=false; // Stop overriding the default RF mode

    // First let's deal with that extra 30 minutes
    if(feedDelay) {
      feedDelay=false; // Reset the feedDelay flag
      setRFmode(Smart_NTM,vtNTMSpeed,vtNTMDuration); // Smart_NTM time!
    } else {
      // Otherwise go to Previous settings
      setRFmode(vtPrevMode,vtPrevSpeed,vtPrevDuration); 
    }
  } else   {
    setRFmode(); // Update the mode if we change it remotely
  }
  
  // ATO Refill mode. Top off ATO reservoir until it's at 100%    
  if (InternalMemory.read(Mem_B_RefillATO)==1) {
     if (ReefAngel.WaterLevel.GetLevel()<100) {
       ReefAngel.Relay.On(Extension);
     } else {
       ReefAngel.Relay.Off(Extension);
       InternalMemory.write(Mem_B_RefillATO, 0);
     }
  }

  // Turn off return pump if we run out of water!
  if (ReefAngel.LowATO.IsActive()) {
    bitClear(ReefAngel.Relay.RelayMaskOff,Return-1);
  } 
  
  if (ReefAngel.DisplayedMenu==WATERCHANGE_MODE) {
    // Start automatic water change here.
    // This function is currently modified to work with the float switch as-is.
    ReefAngel.SingleATOHigh(Extension); // Refill from SW bucket

    wcReady=InternalMemory.read(Mem_B_WaterChange); // Trigger to start
    wcFillTime=InternalMemory.read_int(Mem_I_WCFillTime); 
    // Let's get started
    if(wcReady) {
      wcTimer.SetInterval(wcFillTime); // One bucket at a time
      wcTimer.Start();
      InternalMemory.write(Mem_B_WaterChange, 0);
      bitSet(ReefAngel.Relay.RelayMaskOff,Reactor-1); // Start draining
    } 
    if(wcTimer.IsTriggered()) {
      bitClear(ReefAngel.Relay.RelayMaskOff,Reactor-1); // Stop draining
    }    
  } else { 
    // Find out if we are on vacation
    ReefAngel.CustomVar[Var_Vacation]=InternalMemory.read(Mem_B_Vacation);
    vacationMode=ReefAngel.CustomVar[Var_Vacation];
 
    // We're on vacation. Keep the ATO reservoir filled.
    if (vacationMode==1) {
      ReefAngel.WaterLevelATO(Extension,30,61,63);
    } else {
      ReefAngel.Relay.Off(Extension);
    }

    // Turn off return if we are somehow overflowing the sump
    if (ReefAngel.HighATO.IsActive()) {
      if (!floatHigh) {
        floatHigh=true;
       ReefAngel.CustomVar[Var_HighATO]=1;
       bitClear(ReefAngel.Relay.RelayMaskOff,Return-1);
     }  
    } else {
      floatHigh=false;
     ReefAngel.CustomVar[Var_HighATO]=0;
    }  
  }
      
  // Turn off Skimmer if Return pump is shutoff.   
  if (bitRead(ReefAngel.Relay.RelayMaskOff,Return-1)==0) {
    bitClear(ReefAngel.Relay.RelayMaskOff,Skimmer-1);
  }
    
  // Power Outage - Only Return Pump should be active
  if (!ReefAngel.Relay.IsRelayPresent(EXP1_RELAY)) // Expansion Relay NOT present
  {
    powerOutage=true;
    ReefAngel.Relay.Off (Skimmer); 
    ReefAngel.Relay.Off (WhiteLEDs); 
    ReefAngel.Relay.Off (BlueLEDs); 
    ReefAngel.Relay.Off (Extension);
    ReefAngel.Relay.Off (Heater);
    ReefAngel.Relay.Off (Refugium); 
    ReefAngel.Relay.Off (Reactor); 
    ReefAngel.CustomVar[Var_Power]=1;
  }
    
  ////// Place your custom code above here

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

// Menu Code
void MenuEntry1() {
  ReefAngel.FeedingModeStart();
}
void MenuEntry2() {
  ReefAngel.WaterChangeModeStart();
}
void MenuEntry3() {
  byte mode,speed,duration;
  byte prevMode,prevSpeed,prevDur;
  
  mode=vtMode;
  mode++;
  
  if (mode > 9) { 
    mode=0; 
    speed=50; duration=0; // Constant
  } else if (mode == 1) { 
    speed=40; duration=0; // Lagoon
  } else if (mode == 2) { 
    speed=45; duration=0; // Reef Crest
  } else if (mode == 3) {  
    speed=55; duration=10; // Short Pulse
  } else if (mode == 4) {
    speed=55; duration=20; // Long Pulse
  } else if (mode == 6) {
    speed=50; duration=10; // Smart_TSM
  } else if (mode == 5) {
    speed=vtNTMSpeed; duration=vtNTMDuration; // Smart_NTM
  } else if (mode == 7) {
    speed=vtNightSpeed; duration=vtNightDuration; // Night
    mode=9; 
  }  

  // Backup the previous modes. We don't want to change the default...
  prevMode=vtPrevMode; prevSpeed=vtPrevSpeed; prevDur=vtPrevDuration;
  setRFmode(mode,speed,duration);
  vtPrevMode=prevMode; vtPrevSpeed=prevSpeed; vtPrevDuration=prevDur;
  setRFtimer();
  
  ReefAngel.DisplayedMenu = RETURN_MAIN_MODE;   
}
void MenuEntry4() {
  // Toggle the refugium light if we choose this menu entry.
  if (bitRead(ReefAngel.Relay.RelayData, Refugium-1)) { // If relay is on.
    // Toggle MaskOff for the light
    bitWrite(ReefAngel.Relay.RelayMaskOff, Refugium-1, 1-bitRead(ReefAngel.Relay.RelayMaskOff, Refugium-1));
  } else { 
    // Toggle the MaskOn for the light
    bitWrite(ReefAngel.Relay.RelayMaskOn, Refugium-1, 1-bitRead(ReefAngel.Relay.RelayMaskOn, Refugium-1));
  }
  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();
  ReefAngel.DisplayedMenu = ALT_SCREEN_MODE;
}
void MenuEntry8() {
  ReefAngel.SetupCalibrateWaterLevel();
  ReefAngel.DisplayedMenu = ALT_SCREEN_MODE;
}
void MenuEntry9() {
  ReefAngel.SetupDateTime();
  ReefAngel.DisplayedMenu = ALT_SCREEN_MODE;
}

// Custom Main Screen
void DrawCustomMain() {
  char buf[16];
  byte x = 5;
  byte y = 2;
  byte t;

  // Main Header
  // ReefAngel.LCD.DrawText(DefaultFGColor, DefaultBGColor, 35, y,"Lee's Reef");
  // Had no room for this anymore :(
  
  // Date+Time
  ReefAngel.LCD.DrawDate(x+1, y);
  ReefAngel.LCD.Clear(COLOR_BLACK, 1, y+9, 128, y+9);
  
  // Param Header
  y+=12; 
  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);
  pingSerial();
    
  /// Display Sunrise / Sunset (to be calculated later...)
  y+=12; t=x;
  sprintf(buf, "%02d:%02d", sl.GetRiseHour(), sl.GetRiseMinute());
  ReefAngel.LCD.DrawText(COLOR_BLACK,DefaultBGColor,t,y,"Rise:"); t+=31;
  ReefAngel.LCD.DrawText(COLOR_RED,DefaultBGColor,t,y,buf); 
  sprintf(buf, "%02d:%02d", sl.GetSetHour(), sl.GetSetMinute()); t+=36;
  ReefAngel.LCD.DrawText(COLOR_BLACK,DefaultBGColor,t,y,"Set:"); t+=25;
  ReefAngel.LCD.DrawText(COLOR_RED,DefaultBGColor,t,y,buf);
  pingSerial();

  // MoonPhase
  y+=10; 
  ReefAngel.LCD.DrawText(0,255,x,y,"Moon:");
  ReefAngel.LCD.Clear(DefaultBGColor,x+32,y,x+(128-x),y+8);
  ReefAngel.LCD.DrawText(COLOR_MAGENTA,255,x+32,y,MoonPhaseLabel());
  pingSerial();
  
  // MoonLight %
  y+=10;
  t = intlength(ReefAngel.PWM.GetDaylightValue()) + 1;  t *= 5;
  ReefAngel.LCD.DrawText(COLOR_BLACK,DefaultBGColor,x,y,"MoonLights:"); 
  ReefAngel.LCD.DrawSingleMonitor(ReefAngel.PWM.GetDaylightValue(), DPColor, x+68, y, 1);
  ReefAngel.LCD.DrawText(DPColor, DefaultBGColor, x+68+t, y, "%");
  pingSerial();

  // Display Water level
  y+=10; t=x;
  ConvertNumToString(buf, ReefAngel.WaterLevel.GetLevel(), 1);
  strcat(buf,"  ");
  ReefAngel.LCD.DrawText(DefaultFGColor,DefaultBGColor,x,y,"AT0 Level:"); t+=60;
  ReefAngel.LCD.DrawText(DefaultFGColor,DefaultBGColor,t,y,buf);

  // Vortech Mode
  y+=10; t=x;
  ReefAngel.LCD.DrawText(0,255,x,y,"RF:"); t+=20;
  ReefAngel.LCD.Clear(DefaultBGColor,t,y,x+(128-x),y+8);
  if (vtMode == 0) ReefAngel.LCD.DrawLargeText(COLOR_GREEN,255,t,y,"Constant");
  else if(vtMode == 1) ReefAngel.LCD.DrawLargeText(COLOR_GOLD,255,t,y,"Lagoon");
  else if (vtMode == 2) ReefAngel.LCD.DrawLargeText(COLOR_GOLD,255,t,y,"Reef Crest");
  else if (vtMode == 3) ReefAngel.LCD.DrawLargeText(COLOR_RED,255,t,y,"Short Pulse");
  else if (vtMode == 4) ReefAngel.LCD.DrawLargeText(COLOR_RED,255,t,y,"Long Pulse");
  else if (vtMode == 5) ReefAngel.LCD.DrawLargeText(COLOR_MAGENTA,255,t,y,"Smart NTM");
  else if (vtMode == 6) ReefAngel.LCD.DrawLargeText(COLOR_MAGENTA,255,t,y,"Tidal Swell");
  else if (vtMode == 9) ReefAngel.LCD.DrawLargeText(COLOR_WHITE,0,t,y,"Night");
  y+=10; t=x;
  ReefAngel.LCD.DrawText(0,255,x,y,"RF Speed:"); t+=60;
  ReefAngel.LCD.Clear(DefaultBGColor,t,y,x+(128-x),y+8);
  ReefAngel.LCD.DrawText(COLOR_BLUE, DefaultBGColor,t,y,InternalMemory.RFSpeed_read()); t+=15;
  ReefAngel.LCD.DrawText(COLOR_BLUE, DefaultBGColor,t,y,"/"); t+=10;
  ReefAngel.LCD.DrawText(COLOR_BLUE, DefaultBGColor,t,y,InternalMemory.RFDuration_read());
  pingSerial();
  
  // Display Water level
  y+=10; t=x;
  if (acclDay > 0) {
    ConvertNumToString(buf, acclDay, 1);
    strcat(buf,"  ");
    ReefAngel.LCD.DrawText(DefaultFGColor,DefaultBGColor,x,y,"Acclimation Day:"); t+=100;
    ReefAngel.LCD.DrawText(DefaultFGColor,DefaultBGColor,t,y,buf);
  } else {
    ReefAngel.LCD.Clear(DefaultBGColor,x,y,x+(128-x),y+8);
  }
  
  // Relays
  y+=10; t=x+7;
  byte TempRelay = ReefAngel.Relay.RelayData;
  TempRelay &= ReefAngel.Relay.RelayMaskOff;
  TempRelay |= ReefAngel.Relay.RelayMaskOn;
  ReefAngel.LCD.DrawOutletBox(t, y, TempRelay);
  pingSerial();
  y+=12;
  TempRelay = ReefAngel.Relay.RelayDataE[0];
  TempRelay &= ReefAngel.Relay.RelayMaskOffE[0];
  TempRelay |= ReefAngel.Relay.RelayMaskOnE[0];
  ReefAngel.LCD.DrawOutletBox(t, y, TempRelay);
  pingSerial();
}

void DrawCustomGraph() {
}

byte PWMSlopeOvernight(byte startHour, byte startMinute, byte endHour, byte
endMinute, byte startPWM, byte endPWM, byte Duration, byte oldValue)
{
  
  unsigned long Start = previousMidnight(now())+((unsigned long)NumMins(startHour, startMinute)*60);
  if (hour()<startHour) Start-=86400;
  unsigned long StartD = Start + (Duration*60);
  unsigned long End = nextMidnight(now())+((unsigned long)NumMins(endHour, endMinute)*60);
  if (hour()<startHour) End-=86400;
  unsigned long StopD = End - (Duration*60);
  if ( now() >= Start && now() <= StartD )
    return constrain(map(now(), Start, StartD, startPWM, endPWM),startPWM,
    endPWM);
  else if ( now() >= StopD && now() <= End )
  {
    byte v = constrain(map(now(), StopD, End, startPWM, endPWM),startPWM,
    endPWM);
    return endPWM-v+startPWM;
  }
  else if ( now() > StartD && now() < StopD )
    return endPWM;

  // lastly return the existing value
  return oldValue;
}

// Similar to MoonLights() but adding in ActinicOffset
void RefugiumLights(byte Relay)
{
  int MinuteOffset=InternalMemory.ActinicOffset_read();
  int onTime=NumMins(InternalMemory.StdLightsOffHour_read(),InternalMemory.StdLightsOffMinute_read())+MinuteOffset;
  int offTime=NumMins(InternalMemory.StdLightsOnHour_read(),InternalMemory.StdLightsOnMinute_read())-MinuteOffset;
  
  ReefAngel.StandardLights(Relay,onTime/60,onTime%60,offTime/60,offTime%60);
}

// Vortech Helper functions
void setRFmode(int mode, int speed, int duration) {

  // Check if mode has changed
  if (mode!=vtMode) {  
    vtPrevMode=vtMode;
    vtMode=mode; 
  
    if (mode!=InternalMemory.RFMode_read()) { 
      InternalMemory.RFMode_write(mode); 
    }
    
    // Fix for coming out of night mode
    if (vtPrevMode==Night) {
      ReefAngel.RF.UseMemory=false;
      ReefAngel.RF.SetMode(Feeding_Stop,0,0);
      ReefAngel.RF.UseMemory=true;
    }
    
    // Smart_NTM is on timer mode.
    if (vtMode==Smart_NTM) {
      setRFtimer();
    }
  } 

  // Check if speed has changed
  if (speed!=vtSpeed) {  
    vtPrevSpeed=vtSpeed;
    vtSpeed=speed;
    
    if (speed!=InternalMemory.RFSpeed_read()) {
      InternalMemory.RFSpeed_write(speed);
    } 
  }

  // Check if duration has changed
  if (duration!=vtDuration) {  
    vtPrevDuration=vtDuration;
    vtDuration=duration; 
    
    if (speed!=InternalMemory.RFSpeed_read()) {
      InternalMemory.RFSpeed_write(speed);
    }
  }
}
void setRFmode() {
  setRFmode(InternalMemory.RFMode_read(), InternalMemory.RFSpeed_read(), InternalMemory.RFDuration_read());
}

void setRFtimer(int minutes) {
  rfTimer.SetInterval(minutes*60);
  rfTimer.Start();
  vtOverride=true;
}
void setRFtimer() {
  setRFtimer(60);
}
User avatar
lnevo
Posts: 5430
Joined: Fri Jul 20, 2012 9:42 am

Re: Lee's Feature Complete PDE

Post by lnevo »

Ok, the new version is posted in the second post (http://forum.reefangel.com/viewtopic.php?p=17807#p17807) This version incorporates the Automated Water change mode using my Reactor pump (Media Rinsing and Water change in one!!) It also incorporates the new Tide class and a custom RF mode to use it. The tidal gap (difference between high and low tide) is affected by the current MoonPhase() and the Sync / Anti-Sync pumps will switch direction based on Ebb and Flood of the tide. I also add a PWMSlope to transition to Night Mode and maintain the tidal effect.

Any questions, please ask. Testing so far is going well :)
User avatar
lnevo
Posts: 5430
Joined: Fri Jul 20, 2012 9:42 am

Re: Lee's Feature Complete PDE

Post by lnevo »

Had my night speed PWMSlope getting set wrong... all fixed now :)
User avatar
lnevo
Posts: 5430
Joined: Fri Jul 20, 2012 9:42 am

Re: Lee's Feature Complete PDE

Post by lnevo »

I have turned all my custom code into functions so it should be much easier to borrow bits and pieces. Later, I'll move some of the variables into the functions that don't need to be global to make it even more self-contained.
User avatar
lnevo
Posts: 5430
Joined: Fri Jul 20, 2012 9:42 am

Re: Lee's Feature Complete PDE

Post by lnevo »

Added the anti-sync part of the ReefCrestMode function into play. Added moonrise/set calculation.
User avatar
lnevo
Posts: 5430
Joined: Fri Jul 20, 2012 9:42 am

Re: Lee's Feature Complete PDE

Post by lnevo »

Implemented Roberto's clever multiple screen display feature :)

Roberto... I'm using LCD.DrawGraph and getting a lot of flicker... is there a better way to call that function?
rimai
Posts: 12881
Joined: Fri Mar 18, 2011 6:47 pm

Re: Lee's Feature Complete PDE

Post by rimai »

Graph has it's own custom section...
You need to do this:

Code: Select all

void DrawCustomGraph()
{
  if (ScreenID==1)
    ReefAngel.LCD.DrawGraph(5, 10);
}
Roberto.
User avatar
lnevo
Posts: 5430
Joined: Fri Jul 20, 2012 9:42 am

Re: Lee's Feature Complete PDE

Post by lnevo »

Not working right if I navigate away from the screen and come back it doesn't redraw.... If I make that the default screen then it draws until I leave... how can I trigger it to draw once :)
rimai
Posts: 12881
Joined: Fri Mar 18, 2011 6:47 pm

Re: Lee's Feature Complete PDE

Post by rimai »

You can't have it being called inside DrawCustomMain() at all.
Just on DrawCustomGraph()
Roberto.
User avatar
lnevo
Posts: 5430
Joined: Fri Jul 20, 2012 9:42 am

Lee's Feature Complete PDE

Post by lnevo »

Yeah i took it out...if i navigate off the screen and back its blank...how can i trigger it to draw when switching screens.
rimai
Posts: 12881
Joined: Fri Mar 18, 2011 6:47 pm

Re: Lee's Feature Complete PDE

Post by rimai »

Try this:

Code: Select all

//declare this on global
boolean DrawGraph=true;

// This goes in DrawCustomMain()
  switch (ScreenID)
  {
  case 0:
    {
      break;
    }
  case 1:
    if (DrawGraph)
    {
      ReefAngel.LCD.DrawGraph(5, 10);
      DrawGraph=false;
    }
    break;
  }

  if (ReefAngel.Joystick.IsLeft())
  {
    ReefAngel.ClearScreen(DefaultBGColor);
    DrawGraph=true;
    ScreenID--;
  }
  if (ReefAngel.Joystick.IsRight())
  {
    ReefAngel.ClearScreen(DefaultBGColor);
    DrawGraph=true;
    ScreenID++;
  }
  if (ScreenID<0) ScreenID=NumScreens-1;
  if (ScreenID>=NumScreens) ScreenID=0;
Roberto.
Post Reply