Project in progress

Mega Solar Tracker © GPL3+

Building on and up! Using the lessons learned from the first tracker computer, we combine a bit of everything in this code.

  • 8,219 views
  • 9 comments
  • 51 respects

Components and supplies

Ardgen mega
Arduino Mega 2560 & Genuino Mega 2560
×1
Pos
u-blox Positioning (GNSS, GPS, Glonass, Galileo, Beidou)
Using a NEO 6 or 7 . These are very cool and dead easy to use.
×1
DUAL H-Bridge motor drive
Shield fans have been great actually ... one of my favorite stores. Most of these items can be found there
×1
61pby065esl  sx679  tnr8syww5d
HC-05 Bluetooth Module
×1
DS3231 I2C RTC
×1
Mfr 25frf52 1k sml
Resistor 1k ohm
×1
Mfr 25fbf52 2k21 sml
Resistor 2.21k ohm
×1
Kemet c320c104k5r5ta image
Capacitor 100 nF
×1
LED Matrix I2C interface
Prolly better to use a maxim one as lots of digital I/O on mega. No point in having more things on the I2C bus than necessary
×1
10 DOF gyro / accelerometer board
×1
Buck Power Supply
×1
M6hlqueeutbzfkhid4ph
Arduino Ethernet Shield 2
Ended up doing two version one with the shield and one with the ESP
×1
Cable Gland
×1
IP65 Enclosure
Mine cost under $10 - undoubtedly a second but still fine
×1
Perspex Sheet
Got this from a rubbish bin (i'm a bit of a magpie)
×1
Esp12e
Espressif ESP8266 ESP-12E
Like this but with level sifters to make life easy...
×1

Necessary tools and machines

09507 01
Soldering iron (generic)
Hole Punch or Hole saw
Hand Drill

Apps and online services

About this project

Background

For those how haven’t read the first part of the story.

So as I am coming up to retrofit/overhaul time on the twins, for mechanical reasons the obvious choice is probably the south tower first. It was built with a reversed east-west mechanism. This was a bit of an experiment which served to teach me that there is a good reason for constructing things the "right way." The "twins" are much smaller units being only 1.5KW each, detailed CAD drawings of these are included at the bottom of this project. The two towers feed a single dual input inverter located on the north tower. Each tower has its own battery set, solar regulator and tracking computer. They are however both feed from the same DC pony panels located on the south tower. Both of these are 24V systems not 12V like the east tower.

If your wondering why I'm replacing the optical trackers? This picture on a partially cloudy day shows the problem.

So on my Christmas list for this year was time and location from the GPS, network internet access (NTP time server) and maybe chatting to the inverter to get solar output. I also wanted the unit more compact, preferably in one box so I can mount on the back of the tracker frame much the same as the sensor in the first unit. Of course I’m determined to get the OTA thing right so I don’t have to go up on the power ladder with the laptop to change programs.

More Memory - Bigger CPU

With my UNO on the previous project packed to the rafters, it's time to bring on the Mega. Other contenders for this job are the WeMos (ESP8266) but that will be for the next installment.

The great thing about the Mega chip is that it has four (4) serial ports, a sort of wet dream of serial communications in a chip. So I can hook GPS and inverter plus Bluetooth adapter and still have room for a diagnostic port.

It also has heaps more memory / code space, an essential item for generating a website on chip.

IoT to Replace GUI This Time

Yeah getting with the program, not putting a LCD panel on the unit this time. Little point as it will be up on the tower and you wont be able to read it. Beside thought it might be fun to hook it to the internet and let people see it for a while.

The Code

So we lost 80% of the original code which was GUI. So we gained back a heap memory space removing it but used a shipload more to run the web servers.

We you know how I said no LCD, well I usually cheat and use one during development. Especially as I seemed to have way to many bugs in the system.

Anyhow this was a handy edition while I was getting stuff working. Easy to unplug and the I2C code can actually left in place so I can always plug it back in for bench testing.

I used the TinyGPS as a parser on the GPS NEMA stream. This picked out the 3 bits of information we need for the tracker from the vast array of data available from the u-blox. Talk about using a sledge hammer to crack a peanut, buy hey you can make damn smooth peanut butter while your at it. The GPS lock part seemed quite important it determining if the data was valid so I thought I might indicate the number of satellites on the meatball by lighting up the corners progressively.

Being a newbie to Arduino IoT, the first thing that happened writing the web pages was I ran out of variable memory. Hmmm, how could this be so? I didn’t use many variables of significance. After a bit of bit of “jiggery pokery” and some reading, it turns out the “F()” macro is a handy bit of kit and you should not leave home without it. This tucks Strings back in the “code segment” where they arguably belong. You can see these sprinkled liberally through the code. I dare say it slows things down a bit but I'm tracking the sun not an incoming skud missile so a few extra ms won't hurt.

I must apologize for the web page design, very used to using ASP on a behemoth which is largely running at idle to generate my pages (I’m an old school pure HTML fan). In Arduino land the poor old CPU is screaming it tits off to generate a basic page. A better approach would have been to offload to the web browser using javascript as the host CPU is most likely packing at least 10 times the computer power of the Mega. That said my page is compatible with 99.9% of all devices/browsers as it is pure HTML and requires no other online services to generate. Perfect for stand alone applications.

The web site is also a first cut as I’m still working out what methods work for me in Arduino land. So there are lots of remnants of code samples Frankensteined together with just a little bit of me showing through.

The time stuff was a bit daunting, as there seem to be so many standards and bits to pull together. In the end I managed to work out the how’s and why’s of each part and got the calculations all onto the same time zone base no matter what the incoming source of truth. The tracker uses GPS over Internet NTP over external RTC. This is overkill but the code is written so you can remove a time source in the hardware build and the software is largely OK with that. One issue is that once you tie to a real time source you cant cheat and just put the clock forward to bias the array or compensate for angular offsets. You actually have to use the angle offsets as the clock become an absolute not a variable.

WiFi

Got a working web interface done only to find I really wanted it to be WiFi, talk about any roads and unknown destinations! So Back to looking at the WeMos, Either as shield or a stand alone interfaced via serial or maybe modbus RTU. Another choice was an external ether net bridge, power being the only issue as the one to hand draw around 100 mA. Mind you after a few measurements the arduino ether net shield turns out to be a hungry beast as well.

From my first experience ESP8266 it really seemed to struggle to generate pages, mind you I’ve prolly been using the wrong end of the screwdriver. Definitely an example for where you should off loading the processing to a better CPU or in my case just hold the screwdriver the right way up!

My eBay addiction meant a new WiFi module turned up in short order, it has just a uart, level shifters and that's it. Hooking this to a CH340 and sending it AT commands from one of my favorite tools reveled where and how the time is lost. These work brilliantly if you organised and can spit out all the response in one go, not so good for build on the fly as there is a large sending overhead as you swap modes for sending. This differs from the Arduino ether net shield which does not seem to be as effected, no matter how small a chunk you send. So with lesson learned I got the tracker configuration page generation down from 10's of seconds to seconds. Again this was done in a proof of concept code first before I hacked into the already working Ethernet shield version. The final result being two versions of code for the Mega, one for Ethernet shield and one for ESP WiFi serial adapter.

Enclosure

I decided to put all the electronics except to fuses into a single box which had a clear lid, this allowed me to see my led matrix from underneath. The web interface allows me to swap X-Y axis and flip +/- so I can place the unit anywhere and still have it work.

Clear acrylic sheet was used as a chassis for mounting all the boards too as I wanted this to be transparent to both light and microwaves. One issue I hadn't tested for is if a GPS can see through solar panels, had a plan B of external antenna in mind however. The single chassis layer rapidly turned into 2 layers with all the extra hangers on required for feature creep.

The Ethernet shield was a bit of an ops moment, RJ45 connector up against the edge of the enclosure. Hmmm perhaps we need to change the socket to a vertical mount one or change to a different type of shield which can be mounted on the top layer of the electronics. In the end I split the code into two versions the ethernet and the ESP connect via serial interface. Same web site different output interface.

I thought I would have tons of room in my big box but feature creep is a killer, power supply, RTC, matrix display, GPS and Bluetooth module. One of my colleagues always advises leaving 25% extra enclosure space, that's fine as long as you don't want to put 30% extra in :)

On the towers, actuator cables to the N/S and E/W motors on the tracker have to be re-routed, much less cable is used as they don't need to connect to the control box at the base of the tower. The fuses will mounted externally as we just ran out of room in the box, no big deal and also makes a nice point to disconnect the unit for service. I'm going to run a temporary power cable and attach the unit to the underside of the tracker and do a burn in test. I't will be working but not connected to the motors. I can monitor it and make sure it's 100% before swapping over.

Power consumption be the only issue that has arisen. The total power consumption is higher than desired. I actually thought there might be a short circuit but did some measurements on the input supply at 12V and found.

  • Total power consumption is 250mA, which in this day and age is high.
  • GPS 60 mA
  • WiFi 60 mA - these two are responsible for nearly 50% of the burn!
  • LED display 10-20 mA
  • Sensors 20 mA
  • RS-232 10mA
  • What's left (Mega and H Bridge) 90 mA

Now at 24V whole lot draws about 150 mA so our overnight burn would be between 1.2 to 2.1 AH depending the season.. Hmm we need that to fit in our 20% depth of discharge for the 7/9AH battery. Well almost but if you apply the magic factor of 6 for solar design I need to boost the battery to 20AH ish (not happening!).

So - I think this might be a suck it and see moment. Possible outcomes to come:

  • I can put all the code in an ESP chip and save bucket loads of power.
  • I can implement so power saving code to turn off the GPS overnight.
  • Could run the system without GPS and use the NTP over WiFi to get the time?

Stay tuned for the next installment!

Update March 2019

Part 3 of the story

Code

TRACKER_MEGA_TOP_GPS_PWM_SOFT.inoC/C++
Tracker for Ethernet shield uses both GPS and NTP to retrieve time
#include <SFE_BMP180.h>
#include <avr/wdt.h>
#include <Wire.h>
#include <SPI.h>
#include <LSM303.h>       // modified ... fixed a couple of bugs
#include <LiquidCrystal_I2C.h>
#include <L3G.h>
#include "ds3231.h"
#include <TimeLib.h>
#include "ht16k33.h"
#include <ModbusRtu.h>   // Modified ... this no longer a stock lib - Slave supports register translation/mapping
#include <EEPROM.h>
#include <math.h>
#include <Ethernet.h>
#include <SD.h>
#include <EthernetUdp.h>
#include <TinyGPS.h>

#define ID   1

#define BUFF_MAX 32
#define PARK_EAST 1
#define PARK_WEST 2
#define PARK_NORTH 3
#define PARK_SOUTH 4
#define PARK_FLAT 5

#define MOTOR_DWELL 100

#define MAX_MODBUS_DATA  70

#define HT16K33_DSP_NOBLINK 0   // constants for the half arsed cheapo display
#define HT16K33_DSP_BLINK1HZ 4
#define HT16K33_DSP_BLINK2HZ 2
#define HT16K33_DSP_BLINK05HZ 6


const byte ENCODER_PINA = 2;  // encoder connects - interupt pin
const byte ENCODER_PINB = 3;  // non interupt pin
const byte ENCODER_PB = 8;

const byte RELAY_YZ_DIR = 8; // DIR 1    Y+ Y- East / West       Was the X+ N relay   BROWN
const byte RELAY_YZ_PWM = 7; // PWM 2    Speed East / West       Was the Y- E relay   ORANGE
const byte RELAY_XZ_PWM = 6; // PWM 2    Speed North / South     Was the Y+ W relay   YELLOW
const byte RELAY_XZ_DIR = 5; // DIR 1    X+ X-  North / South    Was the X- S relay   BLUE 


const int chipSelect = 4;

const byte UNUSED09 =  9;  // cycle timer 26 Hz  38ms period
const byte UNUSED10 = 10;
const byte UNUSED11 = 11;
const byte WATCHDOG = 12;

IPAddress ip(192,168,2,177) ;
IPAddress MyDNS(139,130,4,4) ;
unsigned int localPort = 8888;                         // local port to listen for UDP packets
EthernetServer server(80) ;
EthernetClient client ;

byte mac[] = { 0x44, 0x6f, 0x75, 0x67, 0x61, 0x6c };   // Dougal in ASCII
char timeServer[] = "au.pool.ntp.org";                 // time.nist.gov NTP server
const int NTP_PACKET_SIZE = 48;                        // NTP time stamp is in the first 48 bytes of the message
byte packetBuffer[NTP_PACKET_SIZE];                    // buffer to hold incoming and outgoing packets


EthernetUDP Udp;  // A UDP instance to let us send and receive packets over UDP

static bool hasSD = false;
static bool hasNet = false;
static bool hasGyro = false;
static bool hasRTC = false;
static bool hasPres = false ;

L3G gyro;
LSM303 compass;
LiquidCrystal_I2C lcd(0x27);  // Set the LCD I2C address I modified the lib for default wiring

SFE_BMP180 pressure;
HT16K33 HT;

TinyGPS gps;

float ha ;
float sunX ;
float sunrise ;
float sunset ;
Modbus mb_slave(ID, 2, 0); // this is slave ID and RS-232 or USB-FTDI
time_t chiptime ;        
uint8_t rtc_status ;
//uint8_t time[8];
int motor_recycle = 0 ;
char recv[BUFF_MAX];
unsigned int recv_size = 0;
unsigned long prev_millis;
uint8_t u8state; //   machine state
uint8_t u8query; //   pointer to message query
char buff[BUFF_MAX];
char trackername[18] ;
unsigned long  gpschars ; 
float heading ;          // MODBUS MAP
struct ts t;             // 
struct ts td;            //   
struct ts tg;            // 
struct ts tc;            //
int iNightShutdown ;     //
time_t setchiptime ;     // 68    if set to non zero this will trigger a time set event
float zAng ;             // 66
float xMul = 1.0 ;       // 64
float yMul = 1.0 ;       // 62
float zMul = 1.0 ;       // 60
int iXYS = 0 ;           // 59
int iSave = 0 ;          // 58    
int iDoSave = 0 ;        // 57
int iGPSLock = 0  ;      // 56 
unsigned long  fixage ;  // 54
float xRoll = 0.0 ;      // 52
float yRoll = 0.0 ;      // 50
float zRoll = 0.0 ;      // 48
float gT ;               // 46  temp from sensor
float Pr ;               // 44  presure sensor
float alt ;              // 42  altitude from GPS
float T;                 // 40  temperature of board (if has RTC)
float xzTarget ;         // 38  target for angles
float yzTarget ;         // 36
float xzH ;              // 34 hyserisis zone
float yzH ;              // 32
float xzAng;             // 30 current angles
float yzAng;             // 28
float xzOffset;          // 26 offset xz
float yzOffset;          // 24 offset yz
float dyPark;            // 22 parking position
float dxPark;            // 20 
float xMinVal ;          // 18 Min and Max values  X - N/S
float xMaxVal ;          // 16 
float yMinVal ;          // 14  Y  -- E/W
float yMaxVal ;          // 12 
float latitude;          // 10
float longitude;         // 8
int timezone;            // 7
int iDayNight ;          // 6
float solar_az_deg;      // 4
float solar_el_deg;      // 2
int iTrackMode ;         // 1 
int iMode ;              // 0

int iPMode;
int iPWM_YZ ;
int iPWM_XZ ;
int iPowerUp = 0 ;

unsigned long tempus;
int8_t state1 = 0;
int8_t rtc_hour = 0;
int8_t rtc_min = 0 ;
int8_t rtc_sec = 0 ;

void LoadParamsFromEEPROM(bool bLoad){
  if ( bLoad ) {
    xzH = LoadFloatFromEEPROM(0,0.1,20.0,4.0);  // hysterisis NS
    yzH = LoadFloatFromEEPROM(1,0.1,20.0,4.0);  //    ""      EW
  
    dyPark = LoadFloatFromEEPROM(2,-70.0,50.0,0);  
    dxPark = LoadFloatFromEEPROM(3,-5.0,50.0,0.0);  
    
    xzOffset = LoadFloatFromEEPROM(4,-90.0,90.0,0);  // NS
    yzOffset = LoadFloatFromEEPROM(5,-90.0,90.0,0);  // EW
  
    xzTarget = LoadFloatFromEEPROM(6,-90.0,90.0,0);  // NS
    yzTarget = LoadFloatFromEEPROM(7,-90.0,90.0,0);  // EW
  
    xMinVal = LoadFloatFromEEPROM(8,-10.0,60.0,0.0);   // NS
    xMaxVal = LoadFloatFromEEPROM(9,-10.0,60.0,45);
  
    yMinVal = LoadFloatFromEEPROM(10,-70.0,50.0,-65);  // EW
    yMaxVal = LoadFloatFromEEPROM(11,-70.0,50.0,45);
  
    iTrackMode = LoadIntFromEEPROM(12,-1,4,0);
      
    latitude = LoadFloatFromEEPROM(13,-90.0,90.0,-34.051219);
    longitude = LoadFloatFromEEPROM(14,-180.0,180.0,142.013618);
    timezone = LoadIntFromEEPROM(15,0,23,10);  
    xMul = LoadFloatFromEEPROM(16,-10,10,1);  
    yMul = LoadFloatFromEEPROM(17,-10,10,1);  
    zMul = LoadFloatFromEEPROM(18,-10,10,1);  
    iXYS = LoadIntFromEEPROM(19,0,1,0);
    if ( xMul == 0.0 )  // zero is rubbish value so take 1.0 as the default
      xMul = 1.0 ;
    if ( yMul == 0.0 )
      yMul = 1.0 ;
    if ( zMul == 0.0 )
      zMul = 1.0 ;
    iNightShutdown = LoadIntFromEEPROM(20,0,1,1);
    EEPROM.get(0 + (30 * sizeof(float)) , trackername );
  }else{
    EEPROM.put( 0 , xzH );
    EEPROM.put(0 + (1 * sizeof(float)) , yzH );
    EEPROM.put(0 + (2 * sizeof(float)) , dyPark );
    EEPROM.put(0 + (3 * sizeof(float)) , dxPark );
    EEPROM.put(0 + (4 * sizeof(float)) , xzOffset );
    EEPROM.put(0 + (5 * sizeof(float)) , yzOffset );
    EEPROM.put(0 + (6 * sizeof(float)) , xzTarget );
    EEPROM.put(0 + (7 * sizeof(float)) , yzTarget );
    EEPROM.put(0 + (8 * sizeof(float)) , xMinVal );
    EEPROM.put(0 + (9 * sizeof(float)) , xMaxVal );
    EEPROM.put(0 + (10 * sizeof(float)) , yMinVal );
    EEPROM.put(0 + (11 * sizeof(float)) , yMaxVal );
    EEPROM.put(0 + (12 * sizeof(float)) , iTrackMode );
    EEPROM.put(0 + (13 * sizeof(float)) , latitude );
    EEPROM.put(0 + (14 * sizeof(float)) , longitude );
    EEPROM.put(0 + (15 * sizeof(float)) , timezone );  
    EEPROM.put(0 + (16 * sizeof(float)) , xMul );
    EEPROM.put(0 + (17 * sizeof(float)) , yMul );
    EEPROM.put(0 + (18 * sizeof(float)) , zMul );
    EEPROM.put(0 + (19 * sizeof(float)) , iXYS );  
    EEPROM.put(0 + (20 * sizeof(float)) , iNightShutdown );  
    EEPROM.put(0 + (30 * sizeof(float)) , trackername);
  }
}

void setup() {
  int led ;
  
  Wire.begin();
  lcd.begin(20, 4);
  lcd.home();
  lcd.setBacklightPin(3, NEGATIVE);
  lcd.noCursor(); 
  lcd.clear();

  MCUSR &= ~_BV(WDRF);
  wdt_disable();
  
  compass.init();
  compass.enableDefault();
  compass.setTimeout(1000);

  Serial.begin(115200);   // program/debug port
  Serial1.begin(9600);    // GPS port
//  Serial2.begin(115200);  // Modbus Port to esp
  Serial3.begin(115200);  // EPS8266 serial converter
  
  if (gyro.init()) {
    gyro.enableDefault();
    hasGyro = true ;
  }
  if (pressure.begin()){
    Serial.println("BMP180 init success");
    hasPres = true ;
  }      
  pinMode(RELAY_XZ_DIR, OUTPUT); // Outputs for PWM motor control
  pinMode(RELAY_XZ_PWM, OUTPUT); // 
  pinMode(RELAY_YZ_PWM, OUTPUT); //
  pinMode(RELAY_YZ_DIR, OUTPUT); // 
  iPWM_YZ = 0 ;
  iPWM_XZ = 0 ;
  pinMode(13, OUTPUT); //
  digitalWrite(13, HIGH );
  ActivateRelays(0); // call an all stop first

  mb_slave.begin( 9600 ); // RS-232 to base of tower
  tempus = millis() + 100;

  pinMode(UNUSED09, OUTPUT);  // unused so dont leave floating set as output
  pinMode(UNUSED10, OUTPUT);  // 
  pinMode(UNUSED11, OUTPUT);  // 
  pinMode(WATCHDOG, OUTPUT);  // 
  digitalWrite(WATCHDOG,HIGH);

  compass.m_min = (LSM303::vector<int16_t>) {-3848, -1822, -1551 };   // calibration figures are empirical
  compass.m_max = (LSM303::vector<int16_t>) { +3353, +5127, +5300};

  LoadParamsFromEEPROM(true);
 
  DS3231_init(DS3231_INTCN); // look for a rtc
  DS3231_get(&tc);
  DS3231_get(&td);
  rtc_status = DS3231_get_sreg();
  if ((tc.mon == 0 )&& (tc.mday==0)){  // no rtc to load off
    setTime (0,0,0,21,9,2017) ; // midnight on the equinox (will deactivae motors till gets a valid time) ;  
  }else{
    setTime((int)tc.hour,(int)tc.min,(int)tc.sec,(int)tc.mday,(int)tc.mon,(int)tc.year ) ; // set the internal RTC
    hasRTC = true ;
  }

  HT.begin(0x00);
  for (led = 0; led < 127; led++) {
    HT.clearLedNow(led);
  }

  Serial.println("EtherNet init");
  if (SD.begin(chipSelect) || true){
    hasSD = true;
    Serial.println("SD OK");
    Ethernet.begin(mac,ip,MyDNS);
//    Ethernet.begin(mac,ip);
    if (Udp.begin(localPort)==1){
      hasNet = true ;
    }
    server.begin();
  }
  Serial.print("server is at ");
  Serial.println(Ethernet.localIP());


//  wdt_enable(WDTO_8S);
}

// Arduino doesnt have these to we define from a sandard libruary
float arcsin(float x) {
  return (atan(x / sqrt(-x * x + 1)));
}
float arccos(float x) {
  return (atan(x / sqrt(-x * x + 1)) + (2 * atan(1)));
}
// fractional orbital rotation in radians
float gama(struct ts *tm) {
  return ((2 * PI / 365 ) *  DayOfYear(tm->year , tm->mon , tm->mday , tm->hour , tm->min ));
}
// equation of rime
float eqTime(float g) {
  return (229.18 * ( 0.000075 + ( 0.001868 * cos(g)) - (0.032077 * sin(g)) - (0.014615 * cos (2 * g)) - (0.040849 * sin(2 * g))));
}
// declination of sun in radians
float Decl(float g) {
  return ( 0.006918 - (0.399912 * cos(g)) + (0.070257 * sin(g)) - (0.006758 * cos(2 * g)) + ( 0.000907 * sin(2 * g)) - ( 0.002697 * cos(3 * g)) + (0.00148 * sin(3 * g)) );
}
float TimeOffset(float longitude , struct ts *tm ,  int timezone ) {
  float dTmp ;
  dTmp = (-4.0 * longitude ) + (60 * timezone) - eqTime(gama(tm)) ;
  return (dTmp);
}

float TrueSolarTime(float longitude , struct ts *tm ,  int timezone ) {
  float dTmp ;
  dTmp = ( 60.0 * tm->hour ) + (1.0 * tm->min) + (1.0 * tm->sec / 60) - TimeOffset(longitude, tm, timezone) ;
  return (dTmp);
}
float HourAngle(float longitude , struct  ts *tm ,  int timezone) {
  float dTmp;
  dTmp = (TrueSolarTime(longitude, tm, timezone) / 4 ) - 180 ; // 720 minutes is solar noon -- div 4 is 180
  return (dTmp);
}
// Hour angle for sunrise and sunset only
float HA (float lat , struct ts *tm ) {
  float latRad ;
  latRad = lat * 2 * PI / 360 ;
  return ( acos((cos(90.833 * PI / 180 ) / ( cos(latRad) * cos(Decl(gama(tm)))) - (tan(latRad) * tan(Decl(gama(tm)))))) / PI * 180  );
}

float Sunrise(float longitude , float lat , struct ts *tm , int timezone) {
  return (720 - ( 4.0 * (longitude + HA(lat, tm))) + (60 * timezone) - eqTime(gama(tm))  ) ;
}
float Sunset(float longitude , float lat , struct ts *tm , int timezone) {
  return (720 - ( 4.0 * (longitude - HA(lat, tm))) + (60 * timezone) - eqTime(gama(tm))  ) ;
}
float SNoon(float longitude , float lat , struct ts *tm , int timezone) {
  return (720 - ( 4.0 * (longitude  + (60 * timezone) - eqTime(gama(tm))))  ) ;
}

float SolarZenithRad(float longitude , float lat , struct ts *tm , int timezone) {
  float latRad ;
  float decRad ;
  float HourAngleRad ;
  float dTmp ;

  latRad = lat * 2 * PI / 360 ;
  decRad = Decl(gama(tm));
  HourAngleRad = HourAngle (longitude , tm , timezone ) * PI / 180 ;
  dTmp = acos((sin(latRad) * sin(decRad)) + (cos(latRad) * cos(decRad) * cos(HourAngleRad)));
  return (dTmp) ;

}
float SolarElevationRad(float longitude , float lat , struct ts *tm ,  int timezone ) {
  return ((PI / 2) - SolarZenithRad(longitude , lat , tm , timezone )) ;
}

float SolarAzimouthRad(float longitude , float lat , struct ts *tm ,  int timezone) {
  float latRad ;
  float decRad ;
  float solarzenRad ;
  float HourAngleRad ;
  float dTmp ;
  latRad = lat * 2 * PI / 360 ;
  decRad = Decl(gama(tm));
  solarzenRad = SolarZenithRad ( longitude , lat , tm , timezone ) ;
  HourAngleRad = HourAngle (longitude , tm , timezone ) * PI / 180 ;
  dTmp = acos(((sin(decRad) * cos(latRad)) - (cos(HourAngleRad) * cos(decRad) * sin(latRad))) / sin(solarzenRad)) ;
  if ( HourAngleRad < 0 ) {
    return (dTmp) ;
  } else {
    return ((2 * PI) - dTmp) ;
  }
}


void StopYZ(){
  iPWM_YZ=0 ;
  motor_recycle = MOTOR_DWELL ;
}
void StopXZ(){
  iPWM_XZ=0 ;
  motor_recycle = MOTOR_DWELL ;
}

void ActivateRelays(int iAllStop) {
  if (motor_recycle > 0 ){
    motor_recycle-- ;
  }
  if ( iAllStop == 0 ) {
    StopYZ() ;
    StopXZ() ;
  } else {
    if (( iPWM_YZ==0 ) && (motor_recycle == 0 )){
      if (((yzAng  ) < ( yzTarget - yzH )) ) {   // do Y ie E/W before N/S
        digitalWrite(RELAY_YZ_DIR, LOW) ;
        iPWM_YZ=2 ;
      }
      if (((yzAng ) > (yzTarget + yzH )) ) {
        digitalWrite(RELAY_YZ_DIR, HIGH) ;
        iPWM_YZ=2 ;
      }
    }
    if ( iPWM_YZ>0 ){
      if ((yzAng > yzTarget) && ( digitalRead(RELAY_YZ_DIR)==LOW )) {
        StopYZ() ;
      }
      if ((yzAng < yzTarget) && ( digitalRead(RELAY_YZ_DIR)==HIGH )) {
        StopYZ() ;
      }
    }

    if (( iPWM_YZ==0)) {  // if finished on E/W you can do N/S
        if (( iPWM_XZ==0 ) && (motor_recycle == 0 )){
          if ((xzAng < ( xzTarget - xzH ))  )  { // turn on if not in tolerance
            digitalWrite(RELAY_XZ_DIR, LOW) ;
            iPWM_XZ=2 ;
          }
          if ((xzAng > ( xzTarget + xzH )) ) { // turn on if not in tolerance
            digitalWrite(RELAY_XZ_DIR, HIGH) ;
            iPWM_XZ=2 ;
          }
        }
    }else{
      if ((iPWM_XZ>0 )){
        StopXZ() ;
      }
    }
    if ( iPWM_XZ>0 ){
      if ((xzAng > xzTarget ) && ( digitalRead(RELAY_XZ_DIR)==LOW ))  { // if on turn off
        StopXZ() ;
      }
      if ((xzAng < xzTarget ) && ( digitalRead(RELAY_XZ_DIR)==HIGH ))  { // if on turn off
        StopXZ() ;
      }
    }
  }
  if (iPWM_XZ>0){
    iPWM_XZ += 3 ;
  }
  if (iPWM_YZ>0){
    iPWM_YZ += 3 ;
  }
  iPWM_XZ = constrain(iPWM_XZ,0,254);  
  iPWM_YZ = constrain(iPWM_YZ,0,254);  
  analogWrite(RELAY_XZ_PWM,iPWM_XZ);
  analogWrite(RELAY_YZ_PWM,iPWM_YZ);
}

void FloatToModbusWords(float src_value , uint16_t * dest_lo , uint16_t * dest_hi ) {
  uint16_t tempdata[2] ;
  float *tf ;
  tf = (float * )&tempdata[0]  ;
  *tf = src_value ;
  *dest_lo = tempdata[1] ;
  *dest_hi = tempdata[0] ;
}
float FloatFromModbusWords( uint16_t dest_lo , uint16_t dest_hi ) {
  uint16_t tempdata[2] ;
  float *tf ;
  tf = (float * )&tempdata[0]  ;
  tempdata[1] = dest_lo ;
  tempdata[0] = dest_hi  ;
  return (*tf) ;
}


float LoadFloatFromEEPROM(int address,float minval,float maxval, float defaultval){
float tmp ;  
  EEPROM.get(0 + (address * sizeof(float)) , tmp );
  if (( tmp < minval ) || ( tmp > maxval )|| (NumberOK(tmp) == 1)) {
    tmp = defaultval ;
    EEPROM.put(0 + (address * sizeof(float)) , tmp );
  }
  return(tmp);  
}
int LoadIntFromEEPROM(int address,int minval,int maxval, int defaultval){
int tmp ;  
  EEPROM.get(0 + (address * sizeof(float)) , tmp );  // float.. yeah yeah I know... but it makes it compatible with the one above (easy on the brain)
  if (( tmp < minval ) || ( tmp > maxval )) {
    tmp = defaultval ;
    EEPROM.put(0 + (address * sizeof(float)) , tmp );
  }
  return(tmp);  
}

int NumberOK (float target) {
  int tmp = 0 ;
  tmp = isnan(target);
  if ( tmp != 1 ) {
    tmp = isinf(target);
  }
  return (tmp);
}

int SetTimeFromGPS(){
  byte hundredths ;

  gps.crack_datetime((int *)&tg.year,(byte *)&tg.mon,(byte *) &tg.mday,(byte *) &tg.hour,(byte *) &tg.min,(byte *) &tg.sec , &hundredths, &fixage);
  setTime((int)tg.hour,(int)tg.min,(int)tg.sec,(int)tg.mday,(int)tg.mon,(int)tg.year ) ; // set the internal RTC from last GPS time  
  chiptime = now() ;                          // get it back again
  chiptime += (( timezone * SECS_PER_HOUR ) + ( fixage / 1000 )) ; // add the offset plus the fix age
  setTime(chiptime);                         // set it again  
   if (hasRTC) {
      tg.year = year();
      tg.mon = month() ;
      tg.mday = day();
      tg.hour = hour() ;
      tg.min = minute();
      tg.sec = second();
      DS3231_set(tg);                          //should also update this
    }
  return(0);
}


void loop() {
  float P;
  float sunInc;
  float sunAng;
  float xzRatio;
  float yzRatio;
  float decl ;
  float eqtime ;
  float dTmp ;
  float heading ;
  float tst ;
  float flat, flon;
  unsigned short goodsent;
  unsigned short failcs;
//  int iYear , iMon , iMday , iHour , iMin , iSec ;


  if (minute() != rtc_min) { // do onlyonce a minute
    gps.stats(&gpschars, &goodsent , &failcs );
    gps.f_get_position(&flat, &flon,(long unsigned *) &fixage); // return in degrees
    if (hasPres){
      Pr = getPressure((double *)&gT) ;
    }
    if (hasRTC) {
      T = DS3231_get_treg();
    }
    rtc_min = minute() ;
    
    if ((fixage > 0 ) && ( fixage < 40000 )) {   // wait till our fix is valid before we use the values
      latitude = flat ;
      longitude = flon ;
      iGPSLock = gps.satellites() ;
      alt = gps.f_altitude() ;
      if (iPowerUp==0) {   // only do this at startup so we have a better position ref for next time
          EEPROM.put(0 + (13 * sizeof(float)) , latitude );
          EEPROM.put(0 + (14 * sizeof(float)) , longitude );
          iPowerUp = 1 ;
          if (!hasNet ){
            SetTimeFromGPS();
          }
      }
    }else{
      iGPSLock = 0 ;   // if no lock loook at internal clock
    }
  }
  tc.year = year();
  tc.mon = month() ;
  tc.mday = day();
  tc.hour = hour() ;
  tc.min = minute();
  tc.sec = second();
  
  compass.read();  // this reads all 6 channels
  if ( hasGyro ){
    gyro.read();
    xRoll = gyro.g.x ;
    yRoll = gyro.g.y ;
    zRoll = gyro.g.z ;
  }
  
  digitalWrite(UNUSED09,!digitalRead(UNUSED09));   // toggle this output so I can measure the cycle time with a scope
  
  heading = compass.heading((LSM303::vector<int>) { 1, 0, 0 });
  
  if (( compass.a.z != 0) && (!compass.timeoutOccurred() ))  {
    zAng = (float)compass.a.z ;
    if (iXYS == 0 ){                                            // Proper Job make it configurable 
      xzRatio = (float)compass.a.x * xMul / abs(zAng) ;         // Normal
      yzRatio = (float)compass.a.y * yMul / abs(zAng) ;
    }else{
      xzRatio = (float)compass.a.y * xMul / abs(zAng) ;         // Swapped
      yzRatio = (float)compass.a.x * yMul / abs(zAng) ;      
    }
    xzAng = ((float)atan(xzRatio) / PI * 180 ) + xzOffset ;   // good old offsets or fudge factors
    yzAng = ((float)atan(yzRatio) / PI * 180 ) + yzOffset ;
//    digitalWrite(13, LOW);
  }else{                                                       // try restarting the compass/accelerometer modual - cos he gone walkabout...
    Wire.begin();   // reset the I2C
    compass.init();
    compass.enableDefault();
    compass.setTimeout(1000);                                           // BTW I fixed up the int / long issue in the time out function in the LM303 lib I was using
    compass.m_min = (LSM303::vector<int16_t>) {-3848, -1822, -1551 };   // calibration figures are empirical (just whirl it around a bit and records the min max !!)
    compass.m_max = (LSM303::vector<int16_t>) { +3353, +5127, +5300 };
/*    if (tc.sec % 2 == 0 ) {
      digitalWrite(13, HIGH);
    } else {
      digitalWrite(13, LOW);
    }*/
    HT.begin(0x00);
  }
  
  if (setchiptime > 0) {    //  update the arduino time from the modbus register then clear it... also set RTC if fitted
    setTime(setchiptime) ;
    if (hasRTC) {
      tc.year = year();
      tc.mon = month() ;
      tc.mday = day();
      tc.hour = hour() ;
      tc.min = minute();
      tc.sec = second();
      DS3231_set(tc);                          //should also update this
    }    
    setchiptime = 0 ;
  }
  
  if ( hour() != rtc_hour){  // update our time every hour if we can
    if (iGPSLock == 0){
      if( hasNet ){
        sendNTPpacket(timeServer); // send an NTP packet to a time server every hour
      }else{
        if (hasRTC) {
          DS3231_get(&td);
          setTime((int)td.hour,(int)td.min,(int)td.sec,(int)td.mday,(int)td.mon,(int)td.year ) ; // set the internal RTC from Dallas RTC
       }
      }
    }else{
      if ((fixage > 0 ) && ( fixage < 10000 )) {         //        if the lock is less than 10 second old
        SetTimeFromGPS();
      }
    }      
    rtc_hour = hour() ; 
  }

  if ( rtc_sec != second() )  {     //only update once a second
    wdt_reset();                 // reset internal watchdog - good puppy

    if (( tc.sec > 8 ) && ( tc.sec < 58 )) {  // dont calculate arround the minute when time is updating from NTP or GPS as might get a not so funny result
      digitalWrite(13,!digitalRead(13));
      solar_az_deg = SolarAzimouthRad(longitude, latitude, &tc, timezone) * 180 / PI ;
      solar_el_deg = SolarElevationRad(longitude, latitude, &tc, timezone) * 180 / PI ;
    
      decl = Decl(gama(&tc)) * 180 / PI ;
      ha = HourAngle (longitude , &tc , timezone )  ;
      sunrise = Sunrise(longitude, latitude, &tc, timezone) ;
      sunset = Sunset(longitude, latitude, &tc, timezone);
      tst = TrueSolarTime(longitude, &tc, timezone);
      sunX = abs(latitude) + decl ;
      if (solar_el_deg >= 0 ){           // day
        iDayNight = 1 ;
      }else{                             // night
        iDayNight = 0 ;
      }
    }
    switch (iTrackMode) {
      case 4: // both axis to park
        yzTarget = dyPark ;  // night park position  E/W
        xzTarget = dxPark ;  // night park position  N/S
        break ;
      case 3: // both axis off no tracking
        break ;
      case 2: // xz tracking  NS
        if ( iDayNight == 1 ) {
          xzTarget = sunX ; // need to map the coordinate system correctly
        } else {
          xzTarget = dxPark ;  // night park position
        }
        break;
      case 1:  // yz tracking   EW
        if (iDayNight == 1) {
          yzTarget = ha ;
        } else {
          yzTarget = dyPark ;  // night park position
        }
        break;
      case -1: // set target to tracking and park both at nigh
        if (iDayNight == 1) {
          yzTarget = ha ;
          xzTarget = sunX ; // need to map the coordinate system correctly
        } else {
          yzTarget = dyPark ;  // night park position  E/W
          xzTarget = dxPark ;  // night park position  N/S
        }
        break;
      default: // set target to tracking
        if (iDayNight == 1) {
          yzTarget = ha ;
          xzTarget = sunX ; // need to map the coordinate system correctly
        } else {
          yzTarget = dyPark ;  // night park position (dont park the other - leave till morning)
        }
        break;
    }
    xzTarget = constrain(xzTarget,xMinVal,xMaxVal);   // constain function... very cool - dont leave home without it !
    yzTarget = constrain(yzTarget,yMinVal,yMaxVal);

    lcd.setCursor ( 0, 0 );   // Diags in case there is an LCD display attached
    lcd.print("X/Z ");
    if ( xzAng > 0 ) {
      lcd.print("+");
    }
    lcd.print(xzAng);
    lcd.setCursor ( 10, 0 );
    lcd.print("Y/Z ");
    if ( yzAng > 0 ) {
      lcd.print("+");
    }
    lcd.print(yzAng);
    lcd.setCursor ( 0, 1 );
    lcd.print("TX ");
    if (( xzTarget) > 0 ) {
      lcd.print("+");
    }
    lcd.print(( xzTarget));
    lcd.setCursor ( 10, 1 );
    lcd.print("TY ");
    if ((  yzTarget) > 0 ) {
      lcd.print("+");
    }
    lcd.print((  yzTarget));
    lcd.setCursor ( 0, 2 );
    lcd.print("DX ");
    dTmp = ( xzAng - xzTarget) ;
    if (dTmp > 0 ) {
      lcd.print("+");
    }
    lcd.print(dTmp);
    lcd.setCursor ( 10, 2 );
    lcd.print("DY ");
    dTmp = ( yzAng - yzTarget) ;
    if (dTmp > 0 ) {
      lcd.print("+");
    }
    lcd.print(dTmp);

    lcd.setCursor ( 0, 3 );       // line 3
    snprintf(buff, BUFF_MAX, "%02d:%02d:%02d", tc.hour, tc.min, tc.sec);
    lcd.print(buff) ;

    lcd.setCursor ( 9, 3 );       // line 3
    lcd.print( "S") ;
    if ( iDayNight == 0 ) {
      lcd.print( "-") ;
    }else{
      if (( tc.sec % 2 ) == 0 ) {
        lcd.print( "<") ;        
      }else{
        lcd.print( ">") ;
      }
    }
    if ( gps.satellites() > 9 ){
      lcd.print( "-" );
    }else{
      lcd.print(gps.satellites()) ;
    }

    lcd.setCursor ( 13, 3 );       // line 3
    snprintf(buff, BUFF_MAX, "%04X", fixage);
    lcd.print(buff) ;
    
    lcd.setCursor ( 18, 3 );       // line 3
    if ( iPWM_YZ == 0 ) {
      lcd.print( " ") ;
    }else{
      if (( digitalRead(RELAY_YZ_DIR) == LOW )) {
        lcd.print( "W") ;
      }else{
        lcd.print( "E") ;            
      }
    }
    lcd.setCursor ( 19, 3 );       // line 3
    if ( iPWM_XZ == 0 ) {
      lcd.print( " ") ;
    }else{
      if (( digitalRead(RELAY_XZ_DIR) == LOW )) {
        lcd.print( "N") ;
      }else{
        lcd.print( "S") ;            
      }
    }    
    
    rtc_sec = second() ;
    DisplayMeatBall() ;
  }

  if ( iDoSave == 2 ) {  // save them Active via web or 
    LoadParamsFromEEPROM(false);
    iDoSave = 0 ;  // only do once
  }
  if ( iDoSave == 3 ) {  // load them
    LoadParamsFromEEPROM(true);
    iDoSave = 0 ;  // only do once
  }

  if (((tc.hour > 19 ) || ( tc.hour < 5 )) && (iTrackMode < 3)) {
    if ( iNightShutdown != 0 ){
      ActivateRelays(1) ;        
    }else{
      ActivateRelays(0) ;  // power down at night if in tracking mode
    }
  }else{
    ActivateRelays(1) ;    
  }


  state1 = mb_slave.poll( (uint16_t*)&iMode, MAX_MODBUS_DATA );

  switch (state1) {
    case EXC_ADDR_RANGE:
      Serial.println("EXC_ADDR_RANGE PORT 2");
    break;
    case EXC_FUNC_CODE:
      Serial.println("EXC_FUNC_CODE PORT 2");
    break;
    case EXC_REGS_QUANT:
      Serial.println("EXC_REGS_QUANT PORT 2");
    break;
  }
  
  if ( hasNet ) {
    if ( Udp.parsePacket() ) { // check if NTP packet arrived back
      processNTPpacket();
    }
    
    client = server.available();  // process web server
    if (client) {
      processTCPClient(client);
    }  
  }
  
  while (Serial1.available()){ // process the gps buffer
    gps.encode(Serial1.read());
  }
}  //   end of loop 






float DayOfYear(uint16_t iYear , uint8_t iMon , uint8_t iDay , uint8_t iHour , uint8_t iMin ) {
  int i ;
  float iTDay ;

  iTDay = iDay - 1 ;  // this is zero referenced
  for ( i = 1 ; i < iMon ; i++ ) {
    switch (i) {
      case 1:
      case 3:
      case 5:
      case 7:
      case 8:
      case 10:
      case 12:
        iTDay += 31 ;
        break;
      case 4:
      case 6:
      case 9:
      case 11:
        iTDay += 30 ;
        break;
      case 2 :
        if ((iYear % 4) == 0 ) {
          iTDay += 29 ;
        } else {
          iTDay += 28 ;
        }
        break;
    }
  }
  iTDay += (( 1.0 * iHour - 12 ) / 24 ) ;
  //  iDay +=  1.0 * iMin  / 1440 ;
  return (iTDay);
}


int HrsSolarTime(float target) {
  int i ;
  i = target ;
  return (  i / 60 );
}
int MinSolarTime(float target) {
  int i ;
  i = target  ;
  return (   i % 60 );
}

float sign(float target) {
  if (target > 0 ) {
    return (1);
  } else {
    if (target < 0 ) {
      return (-1);
    } else {
      return (0);
    }
  }
}

void DisplayMeatBall() {
  int  pos , led , x , y;
  float dx , dy ;
  float dxa , dya ;

  HT.setBrightness(15);

  dx = xzAng - xzTarget ;
  dy = yzAng - yzTarget ;
  dxa = abs(dx) ;
  dya = abs(dy) ;
  if (dxa < 6) {
    x = 0 ;
  } else {
    if (dxa < 12) {
      x = sign(dx);
    } else {
      if (dxa < 50) {
        x = 2 * sign(dx);
      } else {
        x = 3 * sign(dx);
      }
    }
  }
  if (dya < 6) {
    y = 0 ;
  } else {
    if (dya < 12) {
      y = sign(dy);
    } else {
      if (dya < 25) {
        y = 2 * sign(dy);
      } else {
        y = 3 * sign(dy);
      }
    }
  }
  pos = 27 ; // netral position
  pos += (y * 8) ; // add or sumtract the x in range of -3 to +3
  pos += (x ) ; // add or sumtract 8 * y or y in range of -3 to +3
  for (led = 0; led < 63; led++) {
    switch (led){
       case 0:
       case 7:
       case 56:
       case 63:
       break;
       default:
         HT.clearLedNow(MapLedNo(led));
       break;  
    }
  }

  if ( ++iPMode > 100 ) {
    iPMode = 1 ;
  }
  switch(gps.satellites()){
    case 255:
      HT.clearLedNow(MapLedNo(0)); // turn off four courners
      HT.clearLedNow(MapLedNo(7)); // turn off four courners
      HT.clearLedNow(MapLedNo(63)); // turn off four courners
      HT.clearLedNow(MapLedNo(56)); // turn off four courners
    break;
    case 1:
    case 2:
      HT.setLedNow(MapLedNo(0)); // turn on four courners
      HT.clearLedNow(MapLedNo(7)); // turn on four courners
      HT.clearLedNow(MapLedNo(63)); // turn on four courners
      HT.clearLedNow(MapLedNo(56)); // turn on four courners
    break;
    case 3:
      HT.setLedNow(MapLedNo(0)); // turn on four courners
      HT.setLedNow(MapLedNo(7));
      HT.clearLedNow(MapLedNo(63)); // turn on four courners
      HT.clearLedNow(MapLedNo(56)); // turn on four courners
...

This file has been truncated, please download it to see its full contents.
TRACKER_MEGA_TOP_GPS_PWM_SOFT_ESP.inoC/C++
This is the ESP version .. ie shand Alone Soft AP no NTP requires GPS or RTC
#include <SFE_BMP180.h>
#include <avr/wdt.h>
#include <Wire.h>
#include <SPI.h>
#include <LSM303.h>       // modified ... fixed a couple of bugs
#include <LiquidCrystal_I2C.h>
#include <L3G.h>
#include "ds3231.h"
#include <TimeLib.h>
#include "ht16k33.h"
#include <ModbusRtu.h>   // Modified ... this no longer a stock lib - Slave supports register translation/mapping
#include <EEPROM.h>
#include <math.h>
#include <Ethernet.h>
#include <SD.h>
#include <EthernetUdp.h>
#include <TinyGPS.h>
#include "WiFiEsp.h"

#define ID   1

#define BUFF_MAX 32
#define PARK_EAST 1
#define PARK_WEST 2
#define PARK_NORTH 3
#define PARK_SOUTH 4
#define PARK_FLAT 5

#define MOTOR_DWELL 100

#define MAX_MODBUS_DATA  70

#define HT16K33_DSP_NOBLINK 0   // constants for the half arsed cheapo display
#define HT16K33_DSP_BLINK1HZ 4
#define HT16K33_DSP_BLINK2HZ 2
#define HT16K33_DSP_BLINK05HZ 6


const byte SPARE1 = 2;  
const byte FACTORY_RESET = 3;  
const byte SPARE2 = 8;

const byte RELAY_YZ_DIR = 8; // DIR 1    Y+ Y- East / West       Was the X+ N relay   BROWN
const byte RELAY_YZ_PWM = 7; // PWM 2    Speed East / West       Was the Y- E relay   ORANGE
const byte RELAY_XZ_PWM = 6; // PWM 2    Speed North / South     Was the Y+ W relay   YELLOW
const byte RELAY_XZ_DIR = 5; // DIR 1    X+ X-  North / South    Was the X- S relay   BLUE 

const int chipSelect = 4;
 
const byte UNUSED09 =  9;  // cycle timer 26 Hz  38ms period
const byte UNUSED10 = 10;
const byte UNUSED11 = 11;
const byte WATCHDOG = 12;

IPAddress ip(192,168,42,1) ;
IPAddress CurrentIP ;

char ssid[24] = {"North_Tracker\0"};         // your network SSID (name)
char pass[16] = {"password\0" };             // your network password
int status = WL_IDLE_STATUS;     // the Wifi radio's status
int reqCount = 0;                // number of requests received

static bool hasSD = false;
static bool hasNet = false;
static bool hasGyro = false;
static bool hasRTC = false;
static bool hasPres = false ;

L3G gyro;
LSM303 compass;
LiquidCrystal_I2C lcd(0x27);  // Set the LCD I2C address I modified the lib for default wiring

SFE_BMP180 pressure;
HT16K33 HT;

TinyGPS gps;

WiFiEspServer server(80);
RingBuffer buf(8);

Modbus mb_slave(ID, 2, 0); // this is slave ID and RS-232 or USB-FTDI
time_t chiptime ;        
uint8_t rtc_status ;
uint8_t time[8];
int motor_recycle_x = 0 ;
int motor_recycle_y = 0 ;
char recv[BUFF_MAX];
unsigned int recv_size = 0;
unsigned long prev_millis;
uint8_t u8state; //   machine state
uint8_t u8query; //   pointer to message query
char buff[BUFF_MAX];
char trackername[18] ;
unsigned long  gpschars ; 
float heading ;          // MODBUS MAP
struct ts t;             // 
struct ts td;            //   
struct ts tg;            // 
struct ts tc;            //
float ha ;
float sunX ;
float sunrise ;
float sunset ;
int iNightShutdown ;     //
int iMultiDrive   ;      // 69    do the axis drives run together 
time_t setchiptime ;     // 68    if set to non zero this will trigger a time set event
float zAng ;             // 66
float xMul = 1.0 ;       // 64
float yMul = 1.0 ;       // 62
float zMul = 1.0 ;       // 60
int iXYS = 0 ;           // 59
int iSave = 0 ;          // 58    
int iDoSave = 0 ;        // 57
int iGPSLock = 0  ;      // 56 
unsigned long  fixage ;  // 54
float xRoll = 0.0 ;      // 52
float yRoll = 0.0 ;      // 50
float zRoll = 0.0 ;      // 48
float gT ;               // 46  temp from sensor
float Pr ;               // 44  presure sensor
float alt ;              // 42  altitude from GPS
float T;                 // 40  temperature of board (if has RTC)
float xzTarget ;         // 38  target for angles
float yzTarget ;         // 36
float xzH ;              // 34 hyserisis zone
float yzH ;              // 32
float xzAng;             // 30 current angles
float yzAng;             // 28
float xzOffset;          // 26 offset xz
float yzOffset;          // 24 offset yz
float dyPark;            // 22 parking position
float dxPark;            // 20 
float xMinVal ;          // 18 Min and Max values  X - N/S
float xMaxVal ;          // 16 
float yMinVal ;          // 14  Y  -- E/W
float yMaxVal ;          // 12 
float latitude;          // 10
float longitude;         // 8
int timezone;            // 7
int iDayNight ;          // 6
float solar_az_deg;      // 4
float solar_el_deg;      // 2
int iTrackMode ;         // 1 
int iMode ;              // 0

int iPMode;
int iPWM_YZ ;
int iPWM_XZ ;
int iPowerUp = 0 ;

unsigned long tempus;
int8_t state1 = 0;
int8_t rtc_hour = 0;
int8_t rtc_min = 0 ;
int8_t rtc_sec = 0 ;

void LoadParamsFromEEPROM(bool bLoad){
  if ( bLoad ) {
    xzH = LoadFloatFromEEPROM(0,0.1,20.0,4.0);  // hysterisis NS
    yzH = LoadFloatFromEEPROM(1,0.1,20.0,4.0);  //    ""      EW
  
    dyPark = LoadFloatFromEEPROM(2,-70.0,50.0,0);  
    dxPark = LoadFloatFromEEPROM(3,-5.0,50.0,0.0);  
    
    xzOffset = LoadFloatFromEEPROM(4,-90.0,90.0,0);  // NS
    yzOffset = LoadFloatFromEEPROM(5,-90.0,90.0,0);  // EW
  
    xzTarget = LoadFloatFromEEPROM(6,-90.0,90.0,0);  // NS
    yzTarget = LoadFloatFromEEPROM(7,-90.0,90.0,0);  // EW
  
    xMinVal = LoadFloatFromEEPROM(8,-10.0,60.0,0.0);   // NS
    xMaxVal = LoadFloatFromEEPROM(9,-10.0,60.0,45);
  
    yMinVal = LoadFloatFromEEPROM(10,-70.0,50.0,-65);  // EW
    yMaxVal = LoadFloatFromEEPROM(11,-70.0,50.0,45);
  
    iTrackMode = LoadIntFromEEPROM(12,-1,4,0);
      
    latitude = LoadFloatFromEEPROM(13,-90.0,90.0,-34.051219);
    longitude = LoadFloatFromEEPROM(14,-180.0,180.0,142.013618);
    timezone = LoadIntFromEEPROM(15,0,23,10);  
    xMul = LoadFloatFromEEPROM(16,-10,10,1);  
    yMul = LoadFloatFromEEPROM(17,-10,10,1);  
    zMul = LoadFloatFromEEPROM(18,-10,10,1);  
    iXYS = LoadIntFromEEPROM(19,0,1,0);
    if ( xMul == 0.0 )  // zero is rubbish value so take 1.0 as the default
      xMul = 1.0 ;
    if ( yMul == 0.0 )
      yMul = 1.0 ;
    if ( zMul == 0.0 )
      zMul = 1.0 ;
    iNightShutdown = LoadIntFromEEPROM(20,0,1,1);
    iMultiDrive = LoadIntFromEEPROM(21,0,1,0);
    if (digitalRead(FACTORY_RESET)== LOW) {
        ip= IPAddress(192,168,42,1);
        sprintf(trackername,"Most Excellent\0");
        sprintf(ssid , "Configure\0") ;
        sprintf(pass, "password\0");      
    }else{
      EEPROM.get(0 + (22 * sizeof(float)) , ip );
      if ((( ip[0] == 255 ) && ( ip[1] == 255 ))) {
        ip= IPAddress(192,168,42,1);
      }
      EEPROM.get(0 + (30 * sizeof(float)) , trackername );
      if ( String(trackername).length() < 2 ){
        sprintf(trackername,"Most Excellent\0");
      }
      EEPROM.get(0 + (35 * sizeof(float)) , ssid );
      if (( String(ssid).length() < 2 ) || ((ssid[0] == ssid[11]) && (ssid[14] == ssid[15]) ) ){
        sprintf(ssid , "Configure\0") ;
        sprintf(pass, "password\0");
      }else{
        EEPROM.get(0 + (42 * sizeof(float)) , pass );
      }
    }
  }else{
    EEPROM.put( 0 , xzH );
    EEPROM.put(0 + (1 * sizeof(float)) , yzH );
    EEPROM.put(0 + (2 * sizeof(float)) , dyPark );
    EEPROM.put(0 + (3 * sizeof(float)) , dxPark );
    EEPROM.put(0 + (4 * sizeof(float)) , xzOffset );
    EEPROM.put(0 + (5 * sizeof(float)) , yzOffset );
    EEPROM.put(0 + (6 * sizeof(float)) , xzTarget );
    EEPROM.put(0 + (7 * sizeof(float)) , yzTarget );
    EEPROM.put(0 + (8 * sizeof(float)) , xMinVal );
    EEPROM.put(0 + (9 * sizeof(float)) , xMaxVal );
    EEPROM.put(0 + (10 * sizeof(float)) , yMinVal );
    EEPROM.put(0 + (11 * sizeof(float)) , yMaxVal );
    EEPROM.put(0 + (12 * sizeof(float)) , iTrackMode );
    EEPROM.put(0 + (13 * sizeof(float)) , latitude );
    EEPROM.put(0 + (14 * sizeof(float)) , longitude );
    EEPROM.put(0 + (15 * sizeof(float)) , timezone );  
    EEPROM.put(0 + (16 * sizeof(float)) , xMul );
    EEPROM.put(0 + (17 * sizeof(float)) , yMul );
    EEPROM.put(0 + (18 * sizeof(float)) , zMul );
    EEPROM.put(0 + (19 * sizeof(float)) , iXYS );  
    EEPROM.put(0 + (20 * sizeof(float)) , iNightShutdown );  
    EEPROM.put(0 + (21 * sizeof(float)) , iMultiDrive );  
    EEPROM.put(0 + (22 * sizeof(float)) , ip );  
    EEPROM.put(0 + (30 * sizeof(float)) , trackername);
    EEPROM.put(0 + (35 * sizeof(float)) , ssid);
    EEPROM.put(0 + (42 * sizeof(float)) , pass);
  }
}

void setup() {
  int led ;
  
  Wire.begin();
  lcd.begin(20, 4);
  lcd.home();
  lcd.setBacklightPin(3, NEGATIVE);
  lcd.noCursor(); 
  lcd.clear();

  MCUSR &= ~_BV(WDRF);
  wdt_disable();
  
  compass.init();
  compass.enableDefault();
  compass.setTimeout(1000);

  Serial.begin(115200);   // program/debug port
  Serial1.begin(9600);    // GPS port
//  Serial2.begin(9600);  // Modbus Port
  Serial3.begin(115200);  // EPS8266 serial converter
  
  if (gyro.init()) {
    gyro.enableDefault();
    hasGyro = true ;
  }
  if (pressure.begin()){
    Serial.println("BMP180 init success");
    hasPres = true ;
  }      
  pinMode(FACTORY_RESET,INPUT_PULLUP);
  pinMode(RELAY_XZ_DIR, OUTPUT); // Outputs for PWM motor control
  pinMode(RELAY_XZ_PWM, OUTPUT); // 
  pinMode(RELAY_YZ_PWM, OUTPUT); //
  pinMode(RELAY_YZ_DIR, OUTPUT); // 
  iPWM_YZ = 0 ;
  iPWM_XZ = 0 ;
  pinMode(13, OUTPUT); //
  digitalWrite(13, HIGH );
  ActivateRelays(0); // call an all stop first

  mb_slave.begin( 9600 ); // RS-232 to base of tower
  tempus = millis() + 100;

  pinMode(UNUSED09, OUTPUT);  // unused so dont leave floating set as output
  pinMode(UNUSED10, OUTPUT);  // 
  pinMode(UNUSED11, OUTPUT);  // 
  pinMode(WATCHDOG, OUTPUT);  // 
  digitalWrite(WATCHDOG,HIGH);

  compass.m_min = (LSM303::vector<int16_t>) {-3848, -1822, -1551 };   // calibration figures are empirical
  compass.m_max = (LSM303::vector<int16_t>) { +3353, +5127, +5300};

  LoadParamsFromEEPROM(true);
 
  DS3231_init(DS3231_INTCN); // look for a rtc
  DS3231_get(&tc);
  DS3231_get(&td);
  rtc_status = DS3231_get_sreg();
  if ((tc.mon == 0 )&& (tc.mday==0)){  // no rtc to load off
    setTime (0,0,0,21,9,2017) ; // midnight on the equinox (will deactivae motors till gets a valid time) ;  
  }else{
    setTime((int)tc.hour,(int)tc.min,(int)tc.sec,(int)tc.mday,(int)tc.mon,(int)tc.year ) ; // set the internal RTC
    hasRTC = true ;
  }

  HT.begin(0x00);
  for (led = 0; led < 127; led++) {
    HT.clearLedNow(led);
  }

  Serial3.begin(115200);    // initialize serial for ESP module
  WiFi.init(&Serial3);      // initialize ESP module

  // check for the presence of the shield
  if (WiFi.status() == WL_NO_SHIELD) {
    Serial.println("WiFi shield not present");
//    while (true); // don't continue
  }

  Serial.print("Attempting to start AP ");
  Serial.println(ssid);

  if (( ip[0] == 0 )&&( ip[1] == 0 )&&( ip[2] == 0 )&&( ip[3] == 0 ) && (digitalRead(FACTORY_RESET)== HIGH)){
    WiFi.begin((char*)ssid, (char*)pass) ; 
  }else{
    WiFi.configAP(ip);   // start access point   
    if ( String(pass).length() == 0 ) {
      status = WiFi.beginAP(ssid, 10);    
    }else{
      status = WiFi.beginAP(ssid, 10, pass, ENC_TYPE_WPA2_PSK);
    }
  }
  
  Serial.println("Access point started");
//  printWifiStatus();
    Serial.println("IP address: ");
    CurrentIP = WiFi.localIP() ;
    Serial.println(CurrentIP);
  
  // start the web server on port 80
  server.begin();
  Serial.println("Server started");

//  wdt_enable(WDTO_8S);
}

// Arduino doesnt have these to we define from a sandard libruary
float arcsin(float x) {
  return (atan(x / sqrt(-x * x + 1)));
}
float arccos(float x) {
  return (atan(x / sqrt(-x * x + 1)) + (2 * atan(1)));
}
// fractional orbital rotation in radians
float gama(struct ts *tm) {
  return ((2 * PI / 365 ) *  DayOfYear(tm->year , tm->mon , tm->mday , tm->hour , tm->min ));
}
// equation of rime
float eqTime(float g) {
  return (229.18 * ( 0.000075 + ( 0.001868 * cos(g)) - (0.032077 * sin(g)) - (0.014615 * cos (2 * g)) - (0.040849 * sin(2 * g))));
}
// declination of sun in radians
float Decl(float g) {
  return ( 0.006918 - (0.399912 * cos(g)) + (0.070257 * sin(g)) - (0.006758 * cos(2 * g)) + ( 0.000907 * sin(2 * g)) - ( 0.002697 * cos(3 * g)) + (0.00148 * sin(3 * g)) );
}
float TimeOffset(float longitude , struct ts *tm ,  int timezone ) {
  float dTmp ;
  dTmp = (-4.0 * longitude ) + (60 * timezone) - eqTime(gama(tm)) ;
  return (dTmp);
}

float TrueSolarTime(float longitude , struct ts *tm ,  int timezone ) {
  float dTmp ;
  dTmp = ( 60.0 * tm->hour ) + (1.0 * tm->min) + (1.0 * tm->sec / 60) - TimeOffset(longitude, tm, timezone) ;
  return (dTmp);
}
float HourAngle(float longitude , struct  ts *tm ,  int timezone) {
  float dTmp;
  dTmp = (TrueSolarTime(longitude, tm, timezone) / 4 ) - 180 ; // 720 minutes is solar noon -- div 4 is 180
  return (dTmp);
}
// Hour angle for sunrise and sunset only
float HA (float lat , struct ts *tm ) {
  float latRad ;
  latRad = lat * 2 * PI / 360 ;
  return ( acos((cos(90.833 * PI / 180 ) / ( cos(latRad) * cos(Decl(gama(tm)))) - (tan(latRad) * tan(Decl(gama(tm)))))) / PI * 180  );
}

float Sunrise(float longitude , float lat , struct ts *tm , int timezone) {
  return (720 - ( 4.0 * (longitude + HA(lat, tm))) + (60 * timezone) - eqTime(gama(tm))  ) ;
}
float Sunset(float longitude , float lat , struct ts *tm , int timezone) {
  return (720 - ( 4.0 * (longitude - HA(lat, tm))) + (60 * timezone) - eqTime(gama(tm))  ) ;
}
float SNoon(float longitude , float lat , struct ts *tm , int timezone) {
  return (720 - ( 4.0 * (longitude  + (60 * timezone) - eqTime(gama(tm))))  ) ;
}

float SolarZenithRad(float longitude , float lat , struct ts *tm , int timezone) {
  float latRad ;
  float decRad ;
  float HourAngleRad ;
  float dTmp ;

  latRad = lat * 2 * PI / 360 ;
  decRad = Decl(gama(tm));
  HourAngleRad = HourAngle (longitude , tm , timezone ) * PI / 180 ;
  dTmp = acos((sin(latRad) * sin(decRad)) + (cos(latRad) * cos(decRad) * cos(HourAngleRad)));
  return (dTmp) ;

}
float SolarElevationRad(float longitude , float lat , struct ts *tm ,  int timezone ) {
  return ((PI / 2) - SolarZenithRad(longitude , lat , tm , timezone )) ;
}

float SolarAzimouthRad(float longitude , float lat , struct ts *tm ,  int timezone) {
  float latRad ;
  float decRad ;
  float solarzenRad ;
  float HourAngleRad ;
  float dTmp ;
  latRad = lat * 2 * PI / 360 ;
  decRad = Decl(gama(tm));
  solarzenRad = SolarZenithRad ( longitude , lat , tm , timezone ) ;
  HourAngleRad = HourAngle (longitude , tm , timezone ) * PI / 180 ;
  dTmp = acos(((sin(decRad) * cos(latRad)) - (cos(HourAngleRad) * cos(decRad) * sin(latRad))) / sin(solarzenRad)) ;
  if ( HourAngleRad < 0 ) {
    return (dTmp) ;
  } else {
    return ((2 * PI) - dTmp) ;
  }
}


void StopYZ(){
  iPWM_YZ=0 ;
  motor_recycle_y = MOTOR_DWELL ;
}
void StopXZ(){
  iPWM_XZ=0 ;
  motor_recycle_x = MOTOR_DWELL ;
}

void ActivateRelays(int iAllStop) {
  if (motor_recycle_y > 0 ){
    motor_recycle_y-- ;
  }
  if (motor_recycle_x > 0 ){
    motor_recycle_x-- ;
  }
  if ( iAllStop == 0 ) {
    StopYZ() ;
    StopXZ() ;
  } else {
    if (( iPWM_YZ==0 ) && (motor_recycle_y == 0 )){
      if ((( yzAng  ) < ( yzTarget - yzH )) ) {   // do Y ie E/W before N/S
        digitalWrite(RELAY_YZ_DIR, LOW) ;
        iPWM_YZ=2 ;
      }
      if ((( yzAng ) > ( yzTarget + yzH )) ) {
        digitalWrite(RELAY_YZ_DIR, HIGH) ;
        iPWM_YZ=2 ;
      }
    }
    if ( iPWM_YZ>0 ){
      if ((yzAng > yzTarget) && ( digitalRead(RELAY_YZ_DIR)==LOW )) {
        StopYZ() ;
      }
      if ((yzAng < yzTarget) && ( digitalRead(RELAY_YZ_DIR)==HIGH )) {
        StopYZ() ;
      }
    }

    if (( iPWM_YZ==0) || ( iMultiDrive == 1 )) {  // if finished on E/W you can do N/S  or if we are doing multidrive
        if (( iPWM_XZ==0 ) && (motor_recycle_x == 0 )){
          if ((xzAng < ( xzTarget - xzH ))  )  { // turn on if not in tolerance
            digitalWrite(RELAY_XZ_DIR, LOW) ;
            iPWM_XZ=2 ;
          }
          if ((xzAng > ( xzTarget + xzH )) ) { // turn on if not in tolerance
            digitalWrite(RELAY_XZ_DIR, HIGH) ;
            iPWM_XZ=2 ;
          }
        }
    }else{
      if ((iPWM_XZ>0 )){
        StopXZ() ;
      }
    }
    if ( iPWM_XZ>0 ){
      if ((xzAng > xzTarget ) && ( digitalRead(RELAY_XZ_DIR)==LOW ))  { // if on turn off
        StopXZ() ;
      }
      if ((xzAng < xzTarget ) && ( digitalRead(RELAY_XZ_DIR)==HIGH ))  { // if on turn off
        StopXZ() ;
      }
    }
  }
  if (iPWM_XZ>0){
    iPWM_XZ += 2 ;
  }
  if (iPWM_YZ>0){
    iPWM_YZ += 2 ;
  }
  iPWM_XZ = constrain(iPWM_XZ,0,254);  
  iPWM_YZ = constrain(iPWM_YZ,0,254);  
  analogWrite(RELAY_XZ_PWM,iPWM_XZ);
  analogWrite(RELAY_YZ_PWM,iPWM_YZ);
}

void FloatToModbusWords(float src_value , uint16_t * dest_lo , uint16_t * dest_hi ) {
  uint16_t tempdata[2] ;
  float *tf ;
  tf = (float * )&tempdata[0]  ;
  *tf = src_value ;
  *dest_lo = tempdata[1] ;
  *dest_hi = tempdata[0] ;
}
float FloatFromModbusWords( uint16_t dest_lo , uint16_t dest_hi ) {
  uint16_t tempdata[2] ;
  float *tf ;
  tf = (float * )&tempdata[0]  ;
  tempdata[1] = dest_lo ;
  tempdata[0] = dest_hi  ;
  return (*tf) ;
}


float LoadFloatFromEEPROM(int address,float minval,float maxval, float defaultval){
float tmp ;  
  EEPROM.get(0 + (address * sizeof(float)) , tmp );
  if (( tmp < minval ) || ( tmp > maxval )|| (NumberOK(tmp) == 1)) {
    tmp = defaultval ;
    EEPROM.put(0 + (address * sizeof(float)) , tmp );
  }
  return(tmp);  
}
int LoadIntFromEEPROM(int address,int minval,int maxval, int defaultval){
int tmp ;  
  EEPROM.get(0 + (address * sizeof(float)) , tmp );  // float.. yeah yeah I know... but it makes it compatible with the one above (easy on the brain)
  if (( tmp < minval ) || ( tmp > maxval )) {
    tmp = defaultval ;
    EEPROM.put(0 + (address * sizeof(float)) , tmp );
  }
  return(tmp);  
}

int NumberOK (float target) {
  int tmp = 0 ;
  tmp = isnan(target);
  if ( tmp != 1 ) {
    tmp = isinf(target);
  }
  return (tmp);
}

int SetTimeFromGPS(){
  byte hundredths ;
  tmElements_t tmegps;
  gps.crack_datetime((int *)&tg.year,(byte *)&tg.mon,(byte *) &tg.mday,(byte *) &tg.hour,(byte *) &tg.min,(byte *) &tg.sec , &hundredths, &fixage);
  tmegps.Year = tg.year - 1970 ;
  tmegps.Month = tg.mon ;
  tmegps.Day = tg.mday  ;
  tmegps.Hour = tg.hour ;
  tmegps.Minute = tg.min ;
  tmegps.Second = tg.sec ;
//  setTime((int)tg.hour,(int)tg.min,(int)tg.sec,(int)tg.mday,(int)tg.mon,(int)tg.year ) ; // set the internal RTC from last GPS time  
//  chiptime = now() ;                                                                    // get it back again
  chiptime = makeTime(tmegps);                                     // get the GPS as time_t
  chiptime += (( timezone * SECS_PER_HOUR ) + ( fixage / 1000 )) ; // add the offset plus the fix age
  setTime(chiptime);                         // set it again  
   if (hasRTC) {
      tg.year = year();
      tg.mon = month() ;
      tg.mday = day();
      tg.hour = hour() ;
      tg.min = minute();
      tg.sec = second();
      DS3231_set(tg);                          //should also update this
    }
  return(0);
}


void loop() {
  float P;
  float sunInc;
  float sunAng;
  float xzRatio;
  float yzRatio;
  float decl ;
  float eqtime ;
  float dTmp ;
  float heading ;
  float tst ;
  float flat, flon;
  unsigned short goodsent;
  unsigned short failcs;
  String request ;

//  int iYear , iMon , iMday , iHour , iMin , iSec ;


  if (minute() != rtc_min) { // do onlyonce a minute
    gps.stats(&gpschars, &goodsent , &failcs );
    gps.f_get_position(&flat, &flon,(long unsigned *) &fixage); // return in degrees
    if (hasPres){
      Pr = getPressure((double *)&gT) ;
    }
    if (hasRTC) {
      T = DS3231_get_treg();
    }
    rtc_min = minute() ;
    
    if ((fixage > 0 ) && ( fixage < 40000 )) {   // wait till our fix is valid before we use the values
      latitude = flat ;
      longitude = flon ;
      iGPSLock = gps.satellites() ;
      alt = gps.f_altitude() ;
      if (iPowerUp==0) {   // only do this at startup so we have a better position ref for next time
          EEPROM.put(0 + (13 * sizeof(float)) , latitude );
          EEPROM.put(0 + (14 * sizeof(float)) , longitude );
          iPowerUp = 1 ;
          if (!hasNet ){
            SetTimeFromGPS();
          }
      }
    }else{
      iGPSLock = 0 ;   // if no lock loook at internal clock
    }
  }
  tc.year = year();
  tc.mon = month() ;
  tc.mday = day();
  tc.hour = hour() ;
  tc.min = minute();
  tc.sec = second();
  
  compass.read();  // this reads all 6 channels
  if ( hasGyro ){
    gyro.read();
    xRoll = gyro.g.x ;
    yRoll = gyro.g.y ;
    zRoll = gyro.g.z ;
  }
  
  digitalWrite(UNUSED09,!digitalRead(UNUSED09));   // toggle this output so I can measure the cycle time with a scope
  
  heading = compass.heading((LSM303::vector<int>) { 1, 0, 0 });
  
  if (( compass.a.z != 0) && (!compass.timeoutOccurred() ))  {
    zAng = (float)compass.a.z ;
    if (iXYS == 0 ){                                            // Proper Job make it configurable 
      xzRatio = (float)compass.a.x * xMul / abs(zAng) ;         // Normal
      yzRatio = (float)compass.a.y * yMul / abs(zAng) ;
    }else{
      xzRatio = (float)compass.a.y * xMul / abs(zAng) ;         // Swapped
      yzRatio = (float)compass.a.x * yMul / abs(zAng) ;      
    }
    xzAng = ((float)atan(xzRatio) / PI * 180 ) + xzOffset ;   // good old offsets or fudge factors
    yzAng = ((float)atan(yzRatio) / PI * 180 ) + yzOffset ;
//    digitalWrite(13, LOW);
  }else{                                                       // try restarting the compass/accelerometer modual - cos he gone walkabout...
    Wire.begin();   // reset the I2C
    compass.init();
    compass.enableDefault();
    compass.setTimeout(1000);                                           // BTW I fixed up the int / long issue in the time out function in the LM303 lib I was using
    compass.m_min = (LSM303::vector<int16_t>) {-3848, -1822, -1551 };   // calibration figures are empirical (just whirl it around a bit and records the min max !!)
    compass.m_max = (LSM303::vector<int16_t>) { +3353, +5127, +5300 };
/*    if (tc.sec % 2 == 0 ) {
      digitalWrite(13, HIGH);
    } else {
      digitalWrite(13, LOW);
    }*/
    HT.begin(0x00);
  }
  
  if (setchiptime > 0) {    //  update the arduino time from the modbus register then clear it... also set RTC if fitted
    setTime(setchiptime) ;
    if (hasRTC) {
      tc.year = year();
      tc.mon = month() ;
      tc.mday = day();
      tc.hour = hour() ;
      tc.min = minute();
      tc.sec = second();
      DS3231_set(tc);                          //should also update this
    }    
    setchiptime = 0 ;
  }
  
  if ( hour() != rtc_hour){  // update our time every hour if we can
    if (iGPSLock == 0){
        if (hasRTC) {
          DS3231_get(&td);
          setTime((int)td.hour,(int)td.min,(int)td.sec,(int)td.mday,(int)td.mon,(int)td.year ) ; // set the internal RTC from Dallas RTC
       }
    }else{
      if ((fixage > 0 ) && ( fixage < 10000 )) {         //        if the lock is less than 10 second old
        SetTimeFromGPS();
      }
    }      
    rtc_hour = hour() ; 
  }

  if ( rtc_sec != second() )  {     //only update once a second
    wdt_reset();                 // reset internal watchdog - good puppy

    if (( tc.sec > 8 ) && ( tc.sec < 58 )) {  // dont calculate arround the minute when time is updating from NTP or GPS as might get a not so funny result
      digitalWrite(13,!digitalRead(13));
      solar_az_deg = SolarAzimouthRad(longitude, latitude, &tc, timezone) * 180 / PI ;
      solar_el_deg = SolarElevationRad(longitude, latitude, &tc, timezone) * 180 / PI ;
    
      decl = Decl(gama(&tc)) * 180 / PI ;
      ha = HourAngle (longitude , &tc , timezone )  ;
      sunrise = Sunrise(longitude, latitude, &tc, timezone) ;
      sunset = Sunset(longitude, latitude, &tc, timezone);
      tst = TrueSolarTime(longitude, &tc, timezone);
      sunX = abs(latitude) + decl ;
      if (solar_el_deg >= 0 ){           // day
        iDayNight = 1 ;
      }else{                             // night
        iDayNight = 0 ;
      }
    }
    switch (iTrackMode) {
      case 4: // both axis to park
        yzTarget = dyPark ;  // night park position  E/W
        xzTarget = dxPark ;  // night park position  N/S
        break ;
      case 3: // both axis off no tracking
        break ;
      case 2: // xz tracking  NS
        if ( iDayNight == 1 ) {
          xzTarget = sunX ; // need to map the coordinate system correctly
        } else {
          xzTarget = dxPark ;  // night park position
        }
        break;
      case 1:  // yz tracking   EW
        if (iDayNight == 1) {
          yzTarget = ha ;
        } else {
          yzTarget = dyPark ;  // night park position
        }
        break;
      case -1: // set target to tracking and park both at nigh
        if (iDayNight == 1) {
          yzTarget = ha ;
          xzTarget = sunX ; // need to map the coordinate system correctly
        } else {
          yzTarget = dyPark ;  // night park position  E/W
          xzTarget = dxPark ;  // night park position  N/S
        }
        break;
      default: // set target to tracking
        if (iDayNight == 1) {
          yzTarget = ha ;
          xzTarget = sunX ; // need to map the coordinate system correctly
        } else {
          yzTarget = dyPark ;  // night park position (dont park the other - leave till morning)
        }
        break;
    }
    xzTarget = constrain(xzTarget,xMinVal,xMaxVal);   // constain function... very cool - dont leave home without it !
    yzTarget = constrain(yzTarget,yMinVal,yMaxVal);

    lcd.setCursor ( 0, 0 );   // Diags in case there is an LCD display attached
    lcd.print("X/Z ");
    if ( xzAng > 0 ) {
      lcd.print("+");
    }
    lcd.print(xzAng);
    lcd.setCursor ( 10, 0 );
    lcd.print("Y/Z ");
    if ( yzAng > 0 ) {
      lcd.print("+");
    }
    lcd.print(yzAng);
    lcd.setCursor ( 0, 1 );
    lcd.print("TX ");
    if (( xzTarget) > 0 ) {
      lcd.print("+");
    }
    lcd.print(( xzTarget));
    lcd.setCursor ( 10, 1 );
    lcd.print("TY ");
    if ((  yzTarget) > 0 ) {
      lcd.print("+");
    }
    lcd.print((  yzTarget));
    lcd.setCursor ( 0, 2 );
    lcd.print("DX ");
    dTmp = ( xzAng - xzTarget) ;
    if (dTmp > 0 ) {
      lcd.print("+");
    }
    lcd.print(dTmp);
    lcd.setCursor ( 10, 2 );
    lcd.print("DY ");
    dTmp = ( yzAng - yzTarget) ;
    if (dTmp > 0 ) {
      lcd.print("+");
    }
    lcd.print(dTmp);

    lcd.setCursor ( 0, 3 );       // line 3
    snprintf(buff, BUFF_MAX, "%02d:%02d:%02d", tc.hour, tc.min, tc.sec);
    lcd.print(buff) ;

    lcd.setCursor ( 9, 3 );       // line 3
    lcd.print( "S") ;
    if ( iDayNight == 0 ) {
      lcd.print( "-") ;
    }else{
      if (( tc.sec % 2 ) == 0 ) {
        lcd.print( "<") ;        
      }else{
        lcd.print( ">") ;
      }
    }
    if ( gps.satellites() > 9 ){
      lcd.print( "-" );
    }else{
      lcd.print(gps.satellites()) ;
    }

    lcd.setCursor ( 13, 3 );       // line 3
    snprintf(buff, BUFF_MAX, "%04X", fixage);
    lcd.print(buff) ;
    
    lcd.setCursor ( 18, 3 );       // line 3
    if ( iPWM_YZ == 0 ) {
      lcd.print( " ") ;
    }else{
      if (( digitalRead(RELAY_YZ_DIR) == LOW )) {
        lcd.print( "W") ;
      }else{
        lcd.print( "E") ;            
      }
    }
    lcd.setCursor ( 19, 3 );       // line 3
    if ( iPWM_XZ == 0 ) {
      lcd.print( " ") ;
    }else{
      if (( digitalRead(RELAY_XZ_DIR) == LOW )) {
        lcd.print( "N") ;
      }else{
        lcd.print( "S") ;            
      }
    }    
    
    rtc_sec = second() ;
    DisplayMeatBall() ;
  }

  if ( iDoSave == 2 ) {  // save them Active via web or 
    LoadParamsFromEEPROM(false);
    iDoSave = 0 ;  // only do once
  }
  if ( iDoSave == 3 ) {  // load them
    LoadParamsFromEEPROM(true);
    iDoSave = 0 ;  // only do once
  }

  if (((tc.hour > 19 ) || ( tc.hour < 5 )) && (iTrackMode < 3)) {
    if ( iNightShutdown != 0 ){
      ActivateRelays(1) ;        
    }else{
      ActivateRelays(0) ;  // power down at night if in tracking mode
    }
  }else{
    ActivateRelays(1) ;    
  }


  state1 = mb_slave.poll( (uint16_t*)&iMode, MAX_MODBUS_DATA );

  switch (state1) {
    case EXC_ADDR_RANGE:
      Serial.println("EXC_ADDR_RANGE PORT 2");
    break;
    case EXC_FUNC_CODE:
      Serial.println("EXC_FUNC_CODE PORT 2");
    break;
    case EXC_REGS_QUANT:
      Serial.println("EXC_REGS_QUANT PORT 2");
    break;
  }
  
  
  while (Serial1.available()){ // process the gps buffer
    gps.encode(Serial1.read());
  }

  WiFiEspClient client = server.available();  // listen for incoming clients
  if (client) {                               // if you get a client,
    ActivateRelays(0);                        // deactive motor while we do web
    Serial.println("New client");             // print a message out the serial port
    buf.init();                               // initialize the circular buffer
    request = "" ;
    while (client.connected()) {              // loop while the client's connected
      if (client.available()) {               // if there's bytes to read from the client,
        char c = client.read();               // read a byte, then
        buf.push(c);                          // push it to the ring buffer
        request += c ;

// you got two newline characters in a row
// that's the end of the HTTP request, so send a response
        
        if (buf.endsWith("\r\n\r\n")) {
//        if (request.endsWith("\r\n\r\n")){
//          Serial.print(request);
          processRequest(request);
          if (request.indexOf("favicon.ico")>0){
            sendHttpResponseNG(client);
          }else{
            sendHttpResponse(client);
          }
/*          Serial.println(request.endsWith("\r\n\r\n"));
          Serial.println(request.indexOf("\r\n\r\n"));  */
          client.flush();
          break;
        }
      }
    }
    
    // give the web browser time to receive the data
    delay(10);

    //close the connection
    client.stop();
    Serial.println("Client disconnected");
  }  
}  //   end of loop 






float DayOfYear(uint16_t iYear , uint8_t iMon , uint8_t iDay , uint8_t iHour , uint8_t iMin ) {
  int i ;
  float iTDay ;

  iTDay = iDay - 1 ;  // this is zero referenced
  for ( i = 1 ; i < iMon ; i++ ) {
    switch (i) {
      case 1:
      case 3:
      case 5:
      case 7:
      case 8:
      case 10:
      case 12:
        iTDay += 31 ;
        break;
      case 4:
      case 6:
      case 9:
      case 11:
        iTDay += 30 ;
        break;
      case 2 :
        if ((iYear % 4) == 0 ) {
          iTDay += 29 ;
        } else {
          iTDay += 28 ;
        }
        break;
    }
  }
  iTDay += (( 1.0 * iHour - 12 ) / 24 ) ;
  //  iDay +=  1.0 * iMin  / 1440 ;
  return (iTDay);
}


int HrsSolarTime(float target) {
  int i ;
  i = target ;
  return (  i / 60 );
}
int MinSolarTime(float target) {
  int i ;
  i = target  ;
  return (   i % 60 );
}

float sign(float target) {
  if (target > 0 ) {
    return (1);
  } else {
    if (target < 0 ) {
...

This file has been truncated, please download it to see its full contents.

Custom parts and enclosures

East West axis View
Frame and Piviot
Mounting Frame
North Elevation
Remember this was build by someone from the SOUTHERN hemisphere. All you upside down people need to swap things around
Mounting Post
Yeah this is the pointy end... important to get this bit right
PV panel mounting with respect to the frame

Schematics

Tracker Schematic (mud map)

Comments

Similar projects you might like

Non Optical Solar Tracker (East Tower 2.4KW)

Project in progress by Team Trouble

  • 8,844 views
  • 18 comments
  • 52 respects

Solar Panel Sun Tracker - Phone Charger

Project tutorial by FIELDING

  • 43,989 views
  • 18 comments
  • 172 respects

Solar Tracker 35W with DC Motors

Project showcase by DemetrisEng

  • 8,950 views
  • 4 comments
  • 42 respects

DIY Solar Tracker Arduino Project ITA

Project tutorial by Ingeimaks Ingeimaks

  • 7,734 views
  • 1 comment
  • 27 respects

Dual Axis Solar Tracker Panel with Auto and Manual Mode

Project tutorial by Giannis Arvanitakis

  • 18,002 views
  • 11 comments
  • 63 respects

Solar Tracker V2.0

Project tutorial by Brown Dog Gadgets

  • 17,294 views
  • 3 comments
  • 83 respects
Add projectSign up / Login