My Neptune Apex web interface compatible DIY reef controller

And not need "Daylight Saving Time". I found "#define STDTZOFFSET +8" on Config.h. But the time is daylight time.
 
That is correct.
#define STDTZOFFSET -8L
is my local (Pacific Time in California) timezone, Standard Time.

If your standard time offset is +8, just use
#define STDTZOFFSET 8

The current code has calculation for daylight savings time switch, which will occur next in US on November 3.

I will update the code later with a new #define AUTODST so you can comment that out if your location does not use daylight savings time.

check github later for my update.
 
sdtimer, I have committed the changes to github.

just define STDTZOFFSET 8 for your local timezone and comment the define for AUTODST in config.h since your location does not use DST.
 
I was download new code. It is OK. Thank you very much!

And I have other questiuons

1. I want change outlets schedule to 'start timer&end time'. Not now, I don't know 'INIT OFF & ON & OFF'. Can we use the really time. We have RTC!

2. I found the sunrise&sunset code in this forums, maybe can insert to program.
PHP:
// Natural Reef Aquarium Lighting V2.4 
// 16/06/2013 
// Developed by J. Harp (nUm - RTAW Forums, Numlock10 - Reef Central Forums) 
// Formulas based off of information from NOAA website for sunrise / sunset times. 
// Includes Lunar Simulation. 
// Compiled in Arduino 1.5.2 
// 
// Testing; 
// Additonal colour channels 
// Unique "fullsun" values for each string 
// Fix for 100% power on light start and light stop 
// 
// Future Development: 
// Weather Simulation 
// 
// Please feel free to use this and modify as you see fit, if you have any comments or suggestions please let me know via messages on the forums listed above. 
// 

#include <math.h> 
#include <Wire.h> 
#define DS1307_I2C_ADDRESS 0x68 

// RTC variables 
byte second, rtcMins, oldMins, rtcHrs, oldHrs, dayOfWeek, dayOfMonth, month, year, psecond; 

// LED variables (Change to match your needs) 
byte bluePins[]      =  {3, 9};      // PWM pins for blues 
byte whitePins[]     =  {10, 11};    // PWM pins for whites 
byte uvPins[]        =  {5};         // PWM pins for UVs 
byte moonPins[]      =  {6};            // PWM pins for moonlights 

byte blueChannels    =        2;    // how many PWMs for blues (count from above) 
byte whiteChannels   =        2;    // how many PWMs for whites (count from above) 
byte uvChannels      =        1;    // how many PWMs for uv (count from above) 
byte moonChannels    =        1;    // how many PWMs from moon (count from above) 

byte BluePWMHigh[]          =       {255, 255};        // High value for Blue PWM each vale is for each string - if your values are noraml this is 255, if your values are inverted this is 0 
byte BluePWMLow[]           =       {0, 0};            // Low value for Blue PWM - if your values are noraml this is 0, if your values are inverted this is 255 
float BlueFull[]            =       {25, 25};          // Value in degrees (sun angle) that each Blue string will be at max output 
byte WhitePWMHigh[]         =       {255, 255};        // High value for White PWM - if your values are noraml this is 255, if your values are inverted this is 0 
byte WhitePWMLow[]          =       {0, 0};            // Low value for White PWM - if your values are noraml this is 0, if your values are inverted this is 255 
float WhiteFull[]           =       {37.5, 37.5};      // Value in degrees (sun angle) that each White string will be at max output 
byte UVPWMHigh[]            =       {255};             // High value for UV PWM - if your values are noraml this is 255, if your values are inverted this is 0 
byte UVPWMLow[]             =       {0};               // Low value for UV PWM - if your values are noraml this is 0, if your values are inverted this is 255 
float UVFull[]              =       {30};              // Value in degrees (sun angle) that each UV string will be at max output 
byte MoonPWMHigh[]          =       {255};             // High value for Moon PWM - if your values are noraml this is 255, if your values are inverted this is 0 
byte MoonPWMLow[]           =       {0};               // Low value for Moon PWM - if your values are noraml this is 0, if your values are inverted this is 255 

// Set for the location of the world you want to replicate. 

float latitude = -19.770621;   // + to N  Defualt - (-19.770621) Heart Reef, Great Barrier Reef, QLD, Australia  
float longitude = 149.238532;  // + to E  Defualt - (149.238532) 
int TimeZone = 10;             // + to E  Defulat - (10) 

// Julian Century Varaiable 

float JC; 

// Sunlight Variables 

//float fullSun = 37.5;  // sun elevation in deg that we will assume full sunlight values (Larger = more sunlight) 
int delayTime = 0;     // start time delay in minutes,  - will push the day back, + will bring the day forward 


int SunLight (byte _ledPin, byte _ledHigh, byte _ledLow, float _fullSun, byte _year, byte _month, byte _day, byte _hour, byte _min, byte _sec) 
{ 
  float a = floor((14 - _month)/12); 
  float y = _year + 4800 - a; 
  float m = _month + (12 * a) - 3; 
  float AH; 
  int result; 
   
  JC = (((_day + floor(((153.0 * m) + 2.0) / 5.0) + (365.0 * y) + floor(y / 4.0) - floor(y / 100.0) + floor(y / 400.0) - 32045.0) + ((_hour / 24.0) + (_min / 1444.0) + (_sec / 86400.0))) - 2451556.08) / 36525.0; 
   
  float GMLS = fmod(280.46646+JC*(36000.76983 + JC * 0.0003032),360); 
   
  float GMAS = 357.52911 + JC * (35999.05029 - 0.0001537 * JC); 
   
  float EEO = 0.016708634 - JC * (0.000042037 + 0.0000001267 * JC); 
   
  float SEoC = sin((GMAS * M_PI)/180)*(1.914602 - JC * (0.004817 + 0.000014 * JC)) + sin(((2 * GMAS) * M_PI) / 180) * (0.019993 - 0.000101 * JC) + sin(((3 * JC) * M_PI) / 180) * 0.000289; 
   
  float STL = GMLS + SEoC; 
   
  float STA = GMAS + SEoC; 
   
  float SRV = (1.000001018 * (1 - EEO * EEO)) / (1 + EEO * cos((STA * M_PI) / 180)); 
   
  float SAL = STL - 0.00569 - 0.00478 * sin(((125.04 - 1934.136 * JC) * M_PI) / 180); 
   
  float MOE = 23 + (26 + ((21.448 - JC * (46.815 + JC * (0.00059 - JC * 0.001813)))) / 60) / 60; 
   
  float OC = MOE + 0.00256 * cos(((215.04 - 1934.136 * JC) * M_PI) / 180); 
   
  float SD = (asin(sin((OC * M_PI) / 180) * sin((SAL * M_PI) / 180))) * (180 / M_PI); 
   
  float vy = tan(((OC / 2) * M_PI) / 180) * tan(((OC / 2) * M_PI) / 180); 
   
  float EQoT = (4 * (vy * (sin(2 * ((GMLS * M_PI) / 180)) - 2 * EEO * sin((GMAS * M_PI) / 180) + 4 * EEO * vy * sin((GMAS * M_PI) / 180) * cos( 2 * ((GMLS * M_PI) / 180)) - 0.5 * vy * vy * sin(4 * ((GMLS * M_PI) / 180)) - 1.25 * EEO * EEO * sin(2 * ((GMAS * M_PI) / 180))))) * (180 / M_PI); 
   
  float HAS = acos(cos((90.833 * M_PI) / 180) / (cos((latitude * M_PI) / 180) * cos((SD * M_PI) / 180)) - tan((latitude * M_PI) / 180) * tan((SD * M_PI) / 180)) * (180 / M_PI); 

  float SN = (720 - 4 * longitude - EQoT + TimeZone * 60); 

  float SR = SN - HAS * 4; 

  float SS = SN + HAS * 4; 
  
  float STD = 8 * HAS; 
   
  float TST = fmod((((_hour) + (_min / 60.0) + (_sec / 3600.0)) / 24.0)*1440 + EQoT + 4 * longitude - 60 * TimeZone,1440)+delayTime; 
  
  if (TST / 4 < 0) 
  { 
  AH = ((TST / 4.0) + 180); 
  } 
  else 
  { 
  AH = ((TST / 4.0) - 180);   
  } 
   
  float SZA = (acos(sin((latitude * M_PI) / 180) * sin((SD * M_PI) / 180) + cos((latitude * M_PI) / 180) * cos((SD * M_PI) / 180) * cos((AH * M_PI) / 180))) * (180 / M_PI); 
   
  int SEA = 90 - SZA; 
   
  if (SEA <= 0)   
  { 
   result = _ledLow;     
  } 
   
  if (SEA > 0 && SEA < _fullSun) 
  { 
  result = map(SEA,0,_fullSun,_ledLow,_ledHigh); 
  } 
   
  if (SEA >= _fullSun)   
  { 
  result = _ledHigh; 
  } 
   
  analogWrite(_ledPin, result);   
  return result; 
   
} 

int MoonLight (float JC, byte _ledPin, byte _ledHigh, byte _ledLow) 
{ 
 int result; 
  
 float MS = fmod((2456318.69458333 - JC),29.530589); 
  
 if(MS <= 14.7518) 
 { 
  result = map(MS,0,14.7518,_ledLow,_ledHigh); 
 } 
  
 if( MS > 14.7518) 
 { 
   result = map(MS,14.7518,29.530589,_ledHigh,_ledLow); 
 } 
 analogWrite(_ledPin, result);  
 return result; 
} 
  

/***** RTC Functions *******/ 
/***************************/ 
// Convert normal decimal numbers to binary coded decimal 
byte decToBcd(byte val) 
{ 
  return ( (val/10*16) + (val%10) ); 
} 

// Convert binary coded decimal to normal decimal numbers 
byte bcdToDec(byte val) 
{ 
  return ( (val/16*10) + (val%16) ); 
} 

// Gets the date and time from the ds1307 
void getDateDs1307(byte *second, 
byte *minute, 
byte *hour, 
byte *dayOfWeek, 
byte *dayOfMonth, 
byte *month, 
byte *year) 
{ 
  Wire.beginTransmission(DS1307_I2C_ADDRESS); 
  Wire.write(0); 
  Wire.endTransmission(); 

  Wire.requestFrom(DS1307_I2C_ADDRESS, 7); 

  *second     = bcdToDec(Wire.read() & 0x7f); 
  *minute     = bcdToDec(Wire.read()); 
  *hour       = bcdToDec(Wire.read() & 0x3f); 
  *dayOfWeek  = bcdToDec(Wire.read()); 
  *dayOfMonth = bcdToDec(Wire.read()); 
  *month      = bcdToDec(Wire.read()); 
  *year       = bcdToDec(Wire.read()); 
} 

void setup() { 
delay(500); 
Serial.begin(57600); 
Wire.begin(); 
} 

void loop() { 
  getDateDs1307(&second, &rtcMins, &rtcHrs, &dayOfWeek, &dayOfMonth, &month, &year); 
  if (psecond != second){ 
    psecond = second; 
  Serial.print(rtcHrs); 
  Serial.print(":"); 
  Serial.print(rtcMins); 
  Serial.print(":"); 
  Serial.print(second); 
  Serial.print(" "); 
  Serial.print(dayOfMonth); 
  Serial.print("/"); 
  Serial.print(month); 
  Serial.print("/"); 
  Serial.println(year); 
  update_leds(); 
  } 
} 

void update_leds ( void ){ 
  int i; 
  byte value; 
  int MS; 
  Serial.println("Blue LED's"); 
  for (i = 0; i < blueChannels; i++) 
  { 
    value = SunLight(bluePins[i],BluePWMHigh[i],BluePWMLow[i],BlueFull[i],year,month,dayOfMonth,rtcHrs,rtcMins,second); 
    Serial.print(map(value,BluePWMLow[i],BluePWMHigh[i],0,100)); 
    Serial.print("% "); 
  } 
  Serial.println(); 
  Serial.println("White LED's"); 
  for (i = 0; i < whiteChannels; i++) 
  { 
    value = SunLight(whitePins[i],WhitePWMHigh[i],WhitePWMLow[i],WhiteFull[i],year,month,dayOfMonth,rtcHrs,rtcMins,second); 
    Serial.print(map(value,WhitePWMLow[i],WhitePWMHigh[i],0,100)); 
    Serial.print("% "); 
  } 
  Serial.println(); 
  Serial.println("UV LED's"); 
  for (i = 0; i < uvChannels; i++) 
  { 
    value = SunLight(uvPins[i],UVPWMHigh[i],UVPWMLow[i],UVFull[i],year,month,dayOfMonth,rtcHrs,rtcMins,second); 
    Serial.print(map(value,UVPWMLow[i],UVPWMHigh[i],0,100)); 
    Serial.print("% "); 
  } 
  Serial.println(); 
  Serial.println("Moon Value"); 
  for (i = 0; i < moonChannels; i ++) 
  { 
  MS = MoonLight(JC, moonPins[i],MoonPWMHigh[i],MoonPWMLow[i]); 
  Serial.println(MS); 
  Serial.print(map(MS,MoonPWMLow[i],MoonPWMHigh[i],0,100)); 
  Serial.print("% "); 
  } 
  Serial.println(); 
}
 
The outlet program is for programming the outlets only, the PWM is controlled separately.

The code you posted is for PWM. I currently do not have code for PWM, since I originally planned to use the 2 PWM channel to control LEDs, but I have since bought a Maxspect Razor and I am just using the built in timer and do not need the PWM anymore.

You can try to integrate that PWM code into my code. That code does not use timer interrupts and everything is executed in the main loop so you can simply add the routine to the main loop. My main loop code consumes only approximately 1ms, so you can add that code in no problem.

If I have time, I can write the code for PWM scheduling.
 
I just got the WP-25 I ordered. Delivered in 2 days.

I opened up the controller to check the circuit.
The output control signal is a 3.9 khz pwm signal, 256us period.
It controls the output in 8 steps, starting from 256us for 100% then decrements 16us for each step until it reaches 128us or 50% duty cycle. W2 and W3 are like sawtooth waveform, with W2 incrementing up in 6 steps and down 6 steps, while W3 increments in 6 steps and down in 8 steps from 50% to 100% and back down to 50%. The knob controls the frequency of the sawtooth wave.

this shows the W1 mode low (or pump off), is 1us on to 255us off.
5Bqgt1Z.png


at the lowest time setting (fully counter clockwise) in W1 mode, pump is off for 0.24seconds and on for 0.24seconds. The white block you see is actually comprised of tons of the 1us pulses shown above.
LmEvmIM.png


The output pwm is passed through a resistor /capacitor to convert the pulses to an analog signal.

I think it should be possible to read the control signal using the arduino analog read, and then for Pass-Thru, simply do a digital write to a PWM output pin. I'm not sure if feeding PWM to the pump will work, but it should be easy enough to add a resistor/capacitor to convert it to analog if needed.

I can then program the controller to override the control signal.

The modes on the stock controller seems to work fine. It does not go down below 50% though, except for W1 mode which pulses between 0% and 100%, but the maximum can be controlled via S1 S2 and S3. S1 is 100%, S2 is more like 80% and S3 is more like 60% (the documentation says its 100%, 75% and 50%).

I can see the need to override the stock controller signal say if you got a WP-40 and find it too strong for your tank, as there is currently no way to lower the maximum flow. The S1 S2 S3 flow control only works with W1 mode. I don't think it is proper to be reducing the 24V power to reduce the pump flow as others are doing in the wp-40 thread.

I can implement override to reduce flow to a user controllable value. I don't really see the need to re-invent the different modes that already works fine on the stock controller. I think one cool feature would be to plot in the pump flow level say for the last 3 minutes, which is simply the value of the analog read of the control signal.

So far, I think the WP-25 is a very nice pump, and probably should be a standard issue for any reef tank.
 
Last edited:
I have all my parts now to start making this I hope you find the time to add the pwm to the code for lighting would be nice to add that to the tank
 
I was download new code. It is OK. Thank you very much!

And I have other questiuons

1. I want change outlets schedule to 'start timer&end time'. Not now, I don't know 'INIT OFF & ON & OFF'. Can we use the really time. We have RTC!

2. I found the sunrise&sunset code in this forums, maybe can insert to program.
<math.h><wire.h>

I took a look at the code.
You can revise the sunlight and moonlight functions to simply return result and do the analogWrite separately. You can greatlly simplify this by simply passing the full value (37.5 for white, 25 for blue, 30 for UV), and return the result then do analogWrite using the return value to the PWM pin. You can just use </wire.h></math.h><math.h><wire.h>the year(), month(), day(), hour(), minute() and second() function from the Time library inside the sunlight function. Do not use the RTC code from the program you posted. Max is always 255 and min is always 0.

in the main loop(), just add call to
int result = sunLight(37.5);
analogWrite(pwmpin1, result);
result = sunLight(25);
analogWrite(pwmpin2, result);

There are a lot of floating point operation inside the functions. I don't think you need to call it on every loop. You can call it once a minute.


</wire.h></math.h>
 
on second thought, I think I will implement the WP controller modes in my controller. I'm thinking it would be nice to be able to switch to different modes during the day, which currently cannot be done in the WP controller. Like for example, W1 in the morning, ELSE in the afternoon and H3 at night. The night mode in the controller simply switches to H3 when ambient light level is low.

I'll probably create predefined modes like that ones in the controller, plus a user defined mode. And all modes can have max flow and frequency, plus duty cycle control (the wp controller frequency control is fixed at 50% duty cycle). I'll create 2 channels, and can be operated in independent, sync or anti sync mode (both running exact same program, or the opposite program, i.e. when pump1 is on, pump2 is off)
 
That would be great, I have two WP60 and two WP40 and would love to control them as you describe. Also when I added the heater to outlet 9 and change the max outlet to 16, I couldn't get the webpage to open until I went back to the 8 outlet max. Also the heater doesn't show up in the webpage.
 
ok, if you need to control 4 WP pumps, I will need to move buzzer connection from D5 to D6, so that will free up PWM on D5, D9, D11, D12. D2 and D3 are already used in the 10v PWM. That's all the PWM pins that can be used on the mega.

Get the update to index.htm, and see if it works. I have not tested on 16 outlets
 
I don't need to use any PWM's for lighting I have separate control for them.

that's fine. it just means the controller can have a maximum of 6 pwm lines, and function can be defined by modifying the program. Actually, 3 more can be added for a total of 9 pwm lines if I move the temp sensor onewire line to another pin.
 
Yes in the config.h, I updated to the latest index.htm and when I change to 16 outlets and call out heater on outlet 9 still want open in the webpage. Also I was thinking about making this a slave mega and using a master with a touchscreen lcd to see the ph and temp reading on the touchscreen and everything else on the webpage. So I don't need to use the LCD pin. So can use the rest for the pumps. So far I got the temp and ph reading on my master LCD screen. I control all my lighting on the master with 9 channels of led's using different colors.
 
Yes in the config.h, I updated to the latest index.htm and when I change to 16 outlets and call out heater on outlet 9 still want open in the webpage. Also I was thinking about making this a slave mega and using a master with a touchscreen lcd to see the ph and temp reading on the touchscreen and everything else on the webpage. So I don't need to use the LCD pin. So can use the rest for the pumps. So far I got the temp and ph reading on my master LCD screen. I control all my lighting on the master with 9 channels of led's using different colors.

make sure you change this

#define EEPROMSIG 0xA0 //change this everytime you want the eeprom defaults to change
change it to any value, like 0xA1. That was supposed to be a checksum, but for now, just manually enter a different value.
you will know the EEPROM got updated if you hear the 2 short + 1 long beep confirmation.

changing to use I2C to communicate with another mega will be a big undertaking compared to just getting it to work on the web page. And I think it should be the other way around, the LCD display is the slave, not the master.
 
Last edited:
Back
Top