#define VER "0.06" #define SECONDS_IN_DAY (24 * 60 * 60) #include "ets_sys.h" #include "osapi.h" #include "os_type.h" #include "user_config.h" #include "pgmspace.h" #include #include // ============================== I2C Display #define SDA 5 #define SCL 4 #define CALIBRATE_BUTTON 0 #include // DON't FORGET TO SET in Adafruit+SSD1306.h #define SSD1306_128_64 #include #include #include #include Adafruit_SSD1306 oled(-1); static char dispBuffer[8][21]; // Used to store the screen data to allow asynchronous updates static char *dbuf = &dispBuffer[0][0]; static int displayMode = 0; #define MAX_DISPLAY_MODES 1 #include #include #include #include #include #include #include #include #include //#include //#include #define min(a,b) ((a)<(b)?(a):(b)) const char* password = ""; MDNSResponder mdns; // JSON Parsing buffer StaticJsonBuffer<200> jsonBuffer; static char APssid[64]; static char APpassword[32]; static IPAddress apIP(192, 168, 4, 1); static int wifiChannel, wifiChannelList[20]; static int ssids = 0, ssidList[16]; bool updatedSSID = 0; bool firstWifiCOnnection = 1; bool justBooted = 1; bool gotGPS = 0; // GPS Receiver TinyGPSPlus gps; char latitude[20], longitude[20]; //float dLatitude, dlatitude; int myTimeZone = 0; // flags to allow either MQTT or web access volatile bool useMQTT = 0, useHTML = 1; static int hangUpTimer = 5*60*100; // 5 minutes initail timeout before reset static long millisDelay = 0, oneSecond = 0, tenSeconds = 0, tenMilliseconds = 0, oneHundedMilliseconds = 0, oneMinute = 0, tenMinutes = 0; static int oneHour = 0, oneDay = 0; static int currentAverageCount = 0, actuatorCurrent = 0, baseLineCurrent = 0, cA = 0; static long currentAverage = 0; static int sunsetTimer = -1; // Timer from sunset to dark. -1 = do nothing extern "C" { #include "user_interface.h" } volatile bool tracking = 1; bool panelsToMin = 0, panelsToMax = 0;; static int currentActuator = 4; // Not running static int actuatorStatus = 0; // Actuator speed and direction. 0 = stopped, 1 = full speed extension, -1 = full speed retraction static int actuatorSpeed = 0; // 0=stopped, -100=full speed in, +100=full speed out static int actuatorRunTimer = 0; static int minExtensionLength[3], maxExtensionLength[3]; // min and max extensions of the linear actuators static int upElevateTimer[3], downElevateTimer[3]; static int doUpElevateTimer = 0, doDownElevateTimer = 0; static int calibrate; // Indicates which panel is being calibrated static int doCalibrate = 0; static int calibrateState = 0; // Current state of calibration static int calibrateDelay = 0; // Linear Actuator 0 = ELEVATION #define ELEVATION 0 // Linear actuator 1 = Azimuth #define AZIMUTH 1 static int sunElevation = 0; // Current actual elevation of the sun static int azimuth = 0; // Current azimuth of the sun bool panelMoving = 0; //EEPROM data structure struct { long crc; char ssid[128]; char ssidPassword[128]; char mqttServer[64]; char mqttUser[32]; char mqttPassword[32]; char mqttDevice[32]; char APpassword[32]; char mqttMsg[128]; IPAddress staticIP; IPAddress dns; IPAddress gateway; IPAddress submask; float latitude; float longitude; int upElevateTimer[3]; int downElevateTimer[3]; int maxAngle[3]; int minAngle[3]; int actuatorBaseLength; int actuatorAttachmentHeight; } monitorVariables; // Wifi and destination structure bool updateIntensity = 0; // Time variables #include #include //#include //TimeChangeRule *tcr; //pointer to the time change rule, use to get TZ abbrev //time_t utc, local; // ============================= Function Primitives int angleToActuatorLength(int theta, int whichPanel); char * catString (char *s1, char *s2); int calculateDayOfYear(int day, int month, int year); void eeprom_save(void); int setPanelAngle(int whichPanel, int theta); void setActuator (int whichActuator, char actStatus); char *ftoa(char *a, double f, int precision); time_t tmConvert_t(int YYYY, byte MM, byte DD, byte hh, byte mm, byte ss); void updateDisplay(void); int solar_elevation(float latitude, int dayOfYear, int timeOfDay); static time_t unixTime = 0, measuredTime = 0; static time_t theTime; static int tHour = 0; static uint8_t tMinute = 0, tSecond = 0, tYear = 0, tDay = 0, tMonth = 0, yDay = 0; static struct tm * measuredTimeInfo; static int measuredDay = 0; //#include //WiFiUDP udp; static int sunrise, sunset; static int actualPanelAngle[3]; static int oneMinuteTimer = 60; #define DAY 1 #define NIGHT 0 bool dayOrNight= DAY, lastDayState = DAY; // Some timer counters static int tTimer = 0; static int tenSecondTimer = 0; static int oneSecondTimer = 0; const int led = 2; IPAddress myIP; ESP8266WebServer server(80); static char hostString[32] = {0}; static char outTopic[64]; static char inTopic[64]; static char mqttID[32]; const char* logFile = "/resetLog"; WiFiClient espClient; PubSubClient client(espClient); bool doMQTTUpdate = 0; static int commandSetPanelAngle1 = 0; static int commandSetPanelAngle2 = 0; static int commandSetPanelAngle3 = 0; // Pre-Defined #define PANELUP 'u' #define PANELDOWN 'd' #define PANELSTOP 's' // =============================================== GPIO Pin definitions #define BUTTON 0 #define COMMON_DRIVE 16 #define ACTUATOR1 14 #define ACTUATOR2 12 #define ACTUATOR3 5 #define DAYLIGHT_SENSOR 4 // ======================================== FUNCTIONS =========================================== // Linear actuator Motor Speed interrupt routine LOCAL os_timer_t actuatorTimer; //LOCAL void void ICACHE_RAM_ATTR actuator_timer(void *arg) { // Stop the motors digitalWrite(COMMON_DRIVE, 0); digitalWrite(ACTUATOR1, 0); digitalWrite(ACTUATOR2, 0); digitalWrite(ACTUATOR3, 0); // os_timer_arm_us(&actuatorTimer, delayus, 0); } // Background timer - 1/100 second LOCAL os_timer_t some_timer; static int buttonDebounce = 0; void ICACHE_FLASH_ATTR myTimer(void *arg) { // Button checker if (!digitalRead(BUTTON)) // Button pressed { if (buttonDebounce) --buttonDebounce; else { // if (displayMode <= MAX_DISPLAY_MODES) // ++displayMode; // if (displayMode > MAX_DISPLAY_MODES) // displayMode = 0; doCalibrate = 1; // start off an actuator calibration calibrate = 0; // start with first panel displayMode = 1; // Display the calibration progress // Clear the display for (int y =0; y < 7; y++) for (int x = 0; x < 23; x++) dispBuffer[y][x] = ' '; buttonDebounce = 50; } } // Check to see that we've been getting valid MQTT messages in the last minute, else re-initialise the network if (hangUpTimer) --hangUpTimer; if (!tenSecondTimer--) // Ten seconds countdown { tenSecondTimer = 1000; doMQTTUpdate = 1; } if (calibrateDelay) --calibrateDelay; if (!oneSecondTimer--) // one second countdown { oneSecondTimer = 100; if (sunsetTimer > 0) --sunsetTimer; // Increment the time if (unixTime != 0) { ++unixTime; // Only increment unixtime if it's non-zero } if (measuredTime!= 0) { ++measuredTime; // Only increment measured time if it's non-zero } // Panel calibration timers if (doUpElevateTimer) ++upElevateTimer[(doUpElevateTimer - 1) & 3]; // 0-3 to be sure if (doDownElevateTimer) ++downElevateTimer[(doDownElevateTimer - 1) & 3]; // 0-3 to be sure // Now determine if we are in day or night and set the status accordingly // Increment a 1 second counter if it's dark, decrement it. If it's light, zero it if (--oneMinuteTimer == 0) { int howManyNights = 0; oneMinuteTimer = 60; } } // Check to see if the panel motor is running - i.e. has it hit the end stops currentAverage += (long int) analogRead(A0); if (currentAverageCount++ >= 20) // Calculate the average current taken over 200mS second { currentAverageCount = 0; currentAverage /= 20L; cA = currentAverage; // For display purposes // actuatorCurrent = abs (baseLineCurrent - (int) currentAverage); actuatorCurrent = (int) currentAverage; if (actuatorSpeed == 0) // not moving baseLineCurrent = (int) currentAverage; // Store the baseline current when the actuator isn't running if (actuatorCurrent > 50) panelMoving = 1; else panelMoving = 0; } //digitalWrite(led, digitalRead(DAYLIGHT_SENSOR)); // Show dark/light indicator // See if we are on a timed actuator movement if (actuatorRunTimer) { if (--actuatorRunTimer == 0) actuatorStatus = 0; // stop the movement } switch (actuatorStatus) { case 0: currentActuator = 4; // Switch off the motors break; case PANELSTOP: // Moving from full speed to stop? if (actuatorSpeed > 0) // Allows for speed ramping actuatorSpeed--; if (actuatorSpeed < 0) // Allows for speed ramping actuatorSpeed++; break; case PANELUP: // Moving to full speed UP from stop? if (actuatorSpeed < 100) ++actuatorSpeed; break; case PANELDOWN: // Moving to full speed DOWN from stop if (actuatorSpeed > -100) --actuatorSpeed; break; } // set the direction for the current actuator switch (currentActuator) // 4=none { case 4: // Stop all motors digitalWrite(COMMON_DRIVE, 0); digitalWrite(ACTUATOR1, 0); digitalWrite(ACTUATOR2, 0); digitalWrite(ACTUATOR3, 0); actuatorSpeed = 0; break; case 0: if (actuatorSpeed == 0) // Just stop the motors { digitalWrite(COMMON_DRIVE, 0); digitalWrite(ACTUATOR1, 0); digitalWrite(ACTUATOR2, 0); digitalWrite(ACTUATOR3, 0); } else { if (actuatorSpeed < 0) { digitalWrite(COMMON_DRIVE, 0); digitalWrite(ACTUATOR1, 1); // Only this one is running digitalWrite(ACTUATOR2, 0); digitalWrite(ACTUATOR3, 0); } else { digitalWrite(COMMON_DRIVE, 1); digitalWrite(ACTUATOR1, 0); // Only this one is running digitalWrite(ACTUATOR2, 1); digitalWrite(ACTUATOR3, 1); } } break; case 1: // Actuator 2 if (actuatorSpeed == 0) // Just stop the motors { digitalWrite(COMMON_DRIVE, 0); digitalWrite(ACTUATOR1, 0); digitalWrite(ACTUATOR2, 0); digitalWrite(ACTUATOR3, 0); } else { if (actuatorSpeed < 0) { digitalWrite(COMMON_DRIVE, 0); digitalWrite(ACTUATOR1, 0); // Only this one is running digitalWrite(ACTUATOR2, 1); digitalWrite(ACTUATOR3, 0); } else { digitalWrite(COMMON_DRIVE, 1); digitalWrite(ACTUATOR1, 1); // Only this one is running digitalWrite(ACTUATOR2, 0); digitalWrite(ACTUATOR3, 1); } } break; case 2: // Actuator 3 if (actuatorSpeed == 0) // Just stop the motors { digitalWrite(COMMON_DRIVE, 0); digitalWrite(ACTUATOR1, 0); digitalWrite(ACTUATOR2, 0); digitalWrite(ACTUATOR3, 0); } else { if (actuatorSpeed < 0) { digitalWrite(COMMON_DRIVE, 0); digitalWrite(ACTUATOR1, 0); // Only this one is running digitalWrite(ACTUATOR2, 0); digitalWrite(ACTUATOR3, 1); } else { digitalWrite(COMMON_DRIVE, 1); digitalWrite(ACTUATOR1, 1); // Only this one is running digitalWrite(ACTUATOR2, 1); digitalWrite(ACTUATOR3, 0); } } break; } // Set the delay until the next motor turn off cycle - if it's at maximum speed already, don't set the interrupt, just leave the motor turned on if (abs(actuatorSpeed) < 100) os_timer_arm_us(&actuatorTimer, abs(actuatorSpeed) * 100, 0); } // ================================ MQTT ======================================== static int mqttReconnectAttempts = 0; void ICACHE_FLASH_ATTR reconnect() { if (!client.connected()) { sprintf_P(dispBuffer[6], PSTR("MQTT attempt %d"), mqttReconnectAttempts++); // Attempt to connect if (client.connect(mqttID, monitorVariables.mqttUser, monitorVariables.mqttPassword)) { char s[64]; Serial.println("connected"); // Once connected, publish an announcement... sprintf(s, "Solar Tracker %s Connected - IP address%d.%d.%d.%d", monitorVariables.mqttDevice, WiFi.localIP()[0], WiFi.localIP()[1], WiFi.localIP()[2], WiFi.localIP()[3]); client.publish(outTopic, s); // ... and resubscribe client.subscribe(catString(inTopic, "/#")); } } } //void callback(char* topic, byte* payload, unsigned int length) void callback(char *topic, unsigned char *payload, unsigned int length) { char p[128]; char s[64]; payload[length] = 0; // Terminate payload string if (!strcmp(topic, inTopic)) { JsonObject& root = jsonBuffer.parseObject(payload); sprintf(p,"%s/LOG", outTopic); if (root.success()) { //if (unixTime == 0) // Set unixtime if it's not already set unixTime = root["time"] ; sprintf_P(s, PSTR("Got unixtime %ld"), unixTime); client.publish(p, s); } else { sprintf(s, "====>>>> BAD JSON (%s)", payload); client.publish(p, s); } } else if (!strcmp((char *) topic, catString(inTopic,"/IPAddress"))) { sprintf_P(s, "{\"IPAdress\":\"%d.%d.%d.%d\"}",WiFi.localIP()[0], WiFi.localIP()[1], WiFi.localIP()[2], WiFi.localIP()[3]); client.publish(outTopic, s); } else if (!strcmp((char *) topic, catString(inTopic,"/location"))) { char mylat[32], mylong[32]; ftoa(mylat, monitorVariables.latitude, 2); ftoa(mylong, monitorVariables.longitude, 2); sprintf_P(s, PSTR("{\"longitude\":\"%s\",\"latitude\":\"%s\",\"time\":\"%lu\"}"), mylong, mylat, unixTime); client.publish(outTopic, s); } else if (!strcmp((char *) topic, catString(inTopic,"/reset"))) { oled.clearDisplay(); oled.println("RESETING"); oled.display(); delay(2000); pinMode(0, OUTPUT); digitalWrite(0, 1); delay(1000); ESP.restart(); } else if (!strcmp((char *) topic, catString(inTopic,"/mqttmsg"))) { // Clear the message string just in case for (int i = 0; i < sizeof(monitorVariables.mqttMsg); i++) monitorVariables.mqttMsg[i] = 0; strncpy(monitorVariables.mqttMsg, (char *) payload, length); eeprom_save(); } // Set unit to defaults else if (!strcmp(topic, catString(inTopic, "/defaults"))) { Serial.println("===================== Initialising EEPROM variables"); monitorVariables.crc = 0xCC55; strcpy(monitorVariables.ssid, "xxxx"); // Blank SSID strcpy(monitorVariables.ssidPassword, "Password"); strcpy(monitorVariables.mqttServer, "mqtt.masdelaginebra.com"); strcpy(monitorVariables.mqttUser, "admin"); strcpy(monitorVariables.mqttPassword, "provence2"); sprintf(mqttID, "solar_tracker_%08x", ESP.getChipId()); strcpy(monitorVariables.mqttDevice, mqttID); sprintf(monitorVariables.APpassword, "pass%08x", ESP.getChipId()); strcpy(monitorVariables.mqttMsg, "PIP4048MS Monitor Unit"); monitorVariables.staticIP = IPAddress(0, 0, 0, 0); monitorVariables.submask = IPAddress(0, 0, 0, 0); monitorVariables.dns = IPAddress(0, 0, 0, 0); monitorVariables.gateway = IPAddress(0, 0, 0, 0); monitorVariables.latitude = 55.41L; // Default latitude of Lemmington monitorVariables.longitude = 1.0L; monitorVariables.minAngle[0] = monitorVariables.minAngle[1] = monitorVariables.minAngle[2] = 0; // Default solar panel angles monitorVariables.maxAngle[0] = monitorVariables.maxAngle[1] = monitorVariables.maxAngle[2] = 90; // Default solar panel angles monitorVariables.upElevateTimer[0] = monitorVariables.upElevateTimer[1] = monitorVariables.upElevateTimer[2] = 0; monitorVariables.downElevateTimer[0] = monitorVariables.downElevateTimer[1] = monitorVariables.downElevateTimer[2] = 0; monitorVariables.actuatorAttachmentHeight = 510; monitorVariables.actuatorBaseLength = 800; /* minExtensionLength[0] = angleToActuatorLength(monitorVariables.minAngle[0]); maxExtensionLength[0] = angleToActuatorLength(monitorVariables.maxAngle[0]); minExtensionLength[1] = angleToActuatorLength(monitorVariables.minAngle[1]); maxExtensionLength[1] = angleToActuatorLength(monitorVariables.maxAngle[1]); minExtensionLength[2] = angleToActuatorLength(monitorVariables.minAngle[2]); maxExtensionLength[2] = angleToActuatorLength(monitorVariables.maxAngle[2]); */ eeprom_save(); oled.clearDisplay(); oled.println("RESETING"); oled.display(); yield(); delay(1000); yield(); pinMode(0, INPUT); pinMode(15, INPUT); pinMode(2, INPUT); delay(1000); os_timer_disarm(&some_timer); os_timer_disarm(&actuatorTimer); ESP.restart(); for (;;); // let watchdog reset the chip } else // Force a write to the logfile if (!strcmp(topic, catString(inTopic, "/log/write"))) { File FileHandle = SPIFFS.open(logFile, "a+"); // Open file for reading and appending if (!FileHandle) Serial.println("File NOT opened for storage of reset time"); else { // Save the current time FileHandle.write((byte *) &unixTime, 4); } FileHandle.flush(); FileHandle.close(); } else // Read analog input // Set tracking active or not if (!strcmp(topic, catString(inTopic, "/readcurrent"))) { sprintf(s,"{\"current\", \"%d\"}", actuatorCurrent); client.publish(catString(outTopic,"/analog"), s); } else // Set tracking active or not if (!strcmp(topic, catString(inTopic, "/track"))) tracking = 1; else if (!strcmp(topic, catString(inTopic, "/notrack"))) { tracking = 0; client.publish(catString(outTopic, "/status"), "Tracking OFF"); } else // Command to set panel to a particular angle if (!strcmp(topic, catString(inTopic, "/panelangle1"))) commandSetPanelAngle1 = atoi((char *) payload); else if (!strcmp(topic, catString(inTopic, "/panelangle2"))) commandSetPanelAngle2 = atoi((char *) payload); else if (!strcmp(topic, catString(inTopic, "/panelangle3"))) commandSetPanelAngle3 = atoi((char *) payload); else // Check for panel angle calibration if (!strcmp(topic, catString(inTopic, "/calibrate"))) { doCalibrate = 1; calibrate = 0; } else // Extend actuator 1 if (!strcmp(topic, catString(inTopic, "/actuator1"))) { setActuator(1, toLowerCase(payload[0])); Serial.printf("Actuator 1, %c\n", actuatorStatus); } else if (!strcmp(topic, catString(inTopic, "/actuator2"))) { setActuator(2, toLowerCase(payload[0])); Serial.printf("Actuator 2, %c\n", actuatorStatus); } else if (!strcmp(topic, catString(inTopic, "/actuator3"))) { setActuator(3, toLowerCase(payload[0])); Serial.printf("Actuator 3, %c\n", actuatorStatus); } else if (!strcmp(topic, catString(inTopic, "/log/read"))) { File FileHandle = SPIFFS.open(logFile, "r"); // Open file for reading long record; if (!FileHandle) Serial.println("File NOT opened"); else { // Read the record long recNum = atol((char *) payload); // Convert the payload record number into a long if (recNum >= (FileHandle.size() / 4)) { sprintf(s, "Out of Range"); } else { FileHandle.seek(recNum, SeekSet); // Move to the record position to read if (!FileHandle.read((byte *) &record, 4)) { Serial.println("Couldn't read record"); sprintf(s, "Error reading record"); client.publish(catString(outTopic, "/log/error"), s); } else sprintf(s, "%lu=%lu", recNum, record); } client.publish(catString(outTopic,"/log/read"), s); sprintf(s, "Record number = %ld, payload = %s. Len=%ld", recNum, payload, length); Serial.println(s); } FileHandle.close(); } else // Read the number of records in the logfile if (!strcmp(topic, catString(inTopic, "/log/size"))) { File FileHandle = SPIFFS.open(logFile, "r"); // Open file for reading if (!FileHandle) { Serial.println("File NOT opened"); sprintf(s,"File Not Opened - probably empty"); client.publish(catString(outTopic, "/log/error"), s); } else { sprintf(s, "%lu", FileHandle.size() >> 2); client.publish(catString(outTopic, "/log/size"), s); } } } // ============================= Web pages ====================================== void handleRoot() { static char webPage[6000], s[256]; static int i; bool updateArgs = 0; // TEST ========================= //sprintf(webPage, "UntitledHello kiddies - args = %d", server.args()); //server.send(200, "text/html", webPage); //return; if (server.args() > 0) // got values { // Some commands - track, align panels or calibrate panels if (server.hasArg("doAction")) { if (!strcmp(server.arg("doAction").c_str(), "tracking")) // Enable tracking tracking = 1; if (!strcmp(server.arg("doAction").c_str(), "panelsToMin")) // Enable panel alignment { panelsToMin = 1; tracking = 0; // disable tracking } if (!strcmp(server.arg("doAction").c_str(), "panelsToMax")) // Enable panel alignment { panelsToMax = 1; tracking = 0; // disable tracking } if (!strcmp(server.arg("doAction").c_str(), "calibrate")) // Enable calibration { tracking = 0; // disable tracking doCalibrate = 1; // start off an actuator calibration calibrate = 0; // start with first panel displayMode = 1; // Display the calibration progress } } if (server.hasArg("ssid")) { if (strcmp(monitorVariables.ssid, server.arg("ssid").c_str())) // has ssid been changed? { strcpy(monitorVariables.ssid, server.arg("ssid").c_str()); updateArgs = 1; updatedSSID = 1; } } if (server.hasArg("ssidPassword")) { strcpy(monitorVariables.ssidPassword, server.arg("ssidPassword").c_str()); updateArgs = 1; } if (server.hasArg("mqttServer")) { strcpy(monitorVariables.mqttServer, server.arg("mqttServer").c_str()); updateArgs = 1; } if (server.hasArg("mqttDevice")) { strcpy(monitorVariables.mqttDevice, server.arg("mqttDevice").c_str()); client.unsubscribe(catString(inTopic, "/#")); // Remove old subscription sprintf(inTopic, "%s/toesp", monitorVariables.mqttDevice); // Create new input subscription client.subscribe(catString(inTopic, "/#")); // Add new subscription sprintf(outTopic, "%s/fromesp", monitorVariables.mqttDevice); // Create new output topic updateArgs = 1; } if (server.hasArg("mqttUser")) { strcpy(monitorVariables.mqttUser, server.arg("mqttUser").c_str()); updateArgs = 1; } if (server.hasArg("mqttPassword")) { strcpy(monitorVariables.mqttPassword, server.arg("mqttPassword").c_str()); updateArgs = 1; } if (server.hasArg("APpassword")) { strcpy(monitorVariables.APpassword, server.arg("APpassword").c_str()); updateArgs = 1; } // Static ip if (server.hasArg("staticIP")) { char *ip; strcpy(s, server.arg("staticIP").c_str()); // Split the string on '.' if (strlen(s) == 0) monitorVariables.staticIP = IPAddress(0, 0, 0, 0); else if (ip = strtok(s, ".")) // got a '.'? { for (i = 0; i < 4; i++) { monitorVariables.staticIP[i] = atoi(ip); if ((ip = strtok(NULL, ".")) == NULL) // got another '.'? break; } if (i == 4) // got 4 arguments updateArgs = 1; } } // DNS server if (server.hasArg("dns")) { char *ip; strcpy(s, server.arg("dns").c_str()); // Split the string on '.' if (strlen(s) == 0) monitorVariables.dns = IPAddress(0, 0, 0, 0); else if (ip = strtok(s, ".")) // got a '.'? { for (i = 0; i < 4; i++) { monitorVariables.dns[i] = atoi(ip); if ((ip = strtok(NULL, ".")) == NULL) // got another '.'? break; } if (i == 4) // got 4 arguments updateArgs = 1; } } // Network sub mask if (server.hasArg("submask")) { char *ip; strcpy(s, server.arg("submask").c_str()); // Split the string on '.' if (strlen(s) == 0) monitorVariables.submask = IPAddress(0, 0, 0, 0); else if (ip = strtok(s, ".")) // got a '.'? { for (i = 0; i < 4; i++) { monitorVariables.submask[i] = atoi(ip); if ((ip = strtok(NULL, ".")) == NULL) // got another '.'? break; } if (i == 4) // got 4 arguments updateArgs = 1; } } // Gateway if (server.hasArg("gateway")) { char *ip; strcpy(s, server.arg("gateway").c_str()); // Split the string on '.' if (strlen(s) == 0) monitorVariables.gateway = IPAddress(0, 0, 0, 0); else if (ip = strtok(s, ".")) // got a '.'? { for (i = 0; i < 4; i++) { monitorVariables.gateway[i] = atoi(ip); if ((ip = strtok(NULL, ".")) == NULL) // got another '.'? break; } if (i == 4) // got 4 arguments updateArgs = 1; } } // Site latitude if (server.hasArg("latitude")) { monitorVariables.latitude = atof(server.arg("latitude").c_str()); updateArgs = 1; } // Site longitude if (server.hasArg("longitude")) { monitorVariables.longitude = atof(server.arg("longitude").c_str()); updateArgs = 1; } // Solar panel angles if (server.hasArg("minAngle1")) { monitorVariables.minAngle[0] = atoi(server.arg("minAngle1").c_str()); updateArgs = 1; } if (server.hasArg("minAngle2")) { monitorVariables.minAngle[1] = atoi(server.arg("minAngle2").c_str()); updateArgs = 1; } if (server.hasArg("minAngle3")) { monitorVariables.minAngle[2] = atoi(server.arg("minAngle3").c_str()); updateArgs = 1; } if (server.hasArg("maxAngle1")) { monitorVariables.maxAngle[0] = atoi(server.arg("maxAngle1").c_str()); updateArgs = 1; } if (server.hasArg("maxAngle2")) { monitorVariables.maxAngle[1] = atoi(server.arg("maxAngle2").c_str()); updateArgs = 1; } if (server.hasArg("maxAngle3")) { monitorVariables.maxAngle[2] = atoi(server.arg("maxAngle3").c_str()); updateArgs = 1; } if (server.hasArg("actuatorAttachmentHeight")) { monitorVariables.actuatorAttachmentHeight = atoi(server.arg("actuatorAttachmentHeight").c_str()); updateArgs = 1; } if (server.hasArg("actuatorBaseLength")) { monitorVariables.actuatorBaseLength = atoi(server.arg("actuatorBaseLength").c_str()); updateArgs = 1; } yield(); if (updateArgs) eeprom_save(); Serial.println("\n\nARGUMENTS UPDATED\n\n"); if (updatedSSID) // Have changed the SSID, so we need a reboot { oled.clearDisplay(); oled.println("UPDATED SSID\r\n RESETING"); oled.display(); delay(1000); server.send(200, "text/html", "Wifi SSID updated, resetting. Please reconnect to the new WiFi SSID on this Access Point"); pinMode(0, OUTPUT); digitalWrite(0, 1); delay(1000); ESP.restart(); } } // Send the setup page sprintf(webPage, "Untitled
Solar Panel Sun Tracker Setup - ID %08x
\n", ESP.getChipId()); sprintf(webPage + strlen(webPage), "Network IP Address %d.%d.%d.%d
",WiFi.localIP()[0], WiFi.localIP()[1], WiFi.localIP()[2], WiFi.localIP()[3]); sprintf(webPage + strlen(webPage), "Down1 time=%d, Up1 time=%d, Down2 time=%d, Up2 time=%d
", monitorVariables.downElevateTimer[0], monitorVariables.upElevateTimer[0], monitorVariables.downElevateTimer[1], monitorVariables.upElevateTimer[1]); sprintf(webPage + strlen(webPage), "
%s
\n", monitorVariables.mqttMsg); // Display the available networks for (int netWork = 0; netWork < ssids; netWork++) { if (!strcmp(monitorVariables.ssid, WiFi.SSID(netWork).c_str())) // Select 'checked' wifi network { sprintf(webPage + strlen(webPage), "%s -- strength = %d", WiFi.SSID(netWork).c_str(), WiFi.SSID(netWork).c_str(), (int) WiFi.RSSI(netWork)); // Show connected or not if ((WiFi.status() == WL_CONNECTED)) sprintf(webPage + strlen(webPage), " (CONNECTED)"); sprintf(webPage + strlen(webPage), "
\n"); } else sprintf(webPage + strlen(webPage), "%s -- strength = %d
\n", WiFi.SSID(netWork).c_str(), WiFi.SSID(netWork).c_str(), (int) WiFi.RSSI(netWork)); } sprintf(webPage + strlen(webPage), "
Wifi Password

\n", monitorVariables.ssidPassword); sprintf(webPage + strlen(webPage), "
Solar Tracker Access Point Password

\n", monitorVariables.APpassword); yield(); sprintf(webPage + strlen(webPage), "MQTT Server URL or ip
\n", monitorVariables.mqttServer); sprintf(webPage + strlen(webPage), "MQTT Username
\n", monitorVariables.mqttUser); sprintf(webPage + strlen(webPage), "MQTT Password
\n", monitorVariables.mqttPassword); sprintf(webPage + strlen(webPage), "MQTT Device Name
\n", monitorVariables.mqttDevice); // Now the static IP address info if (monitorVariables.staticIP) // sprintf(webPage + strlen(webPage), "Static IP Address
\n", monitorVariables.staticIP.toString().c_str()); sprintf(webPage + strlen(webPage), "Static IP Address
\n", monitorVariables.staticIP[0], monitorVariables.staticIP[1], monitorVariables.staticIP[2], monitorVariables.staticIP[3]); else sprintf(webPage + strlen(webPage), "Static IP Address
\n"); if (monitorVariables.gateway) sprintf(webPage + strlen(webPage), "Gateway IP Address
\n", monitorVariables.gateway[0], monitorVariables.gateway[1], monitorVariables.gateway[2], monitorVariables.gateway[3]); else sprintf(webPage + strlen(webPage), "Gateway
\n"); if (monitorVariables.submask) sprintf(webPage + strlen(webPage), "Subnet mask
\n", monitorVariables.submask[0], monitorVariables.submask[1], monitorVariables.submask[2], monitorVariables.submask[3]); else sprintf(webPage + strlen(webPage), "Submask
\n"); if (monitorVariables.dns) sprintf(webPage + strlen(webPage), "DNS IP Address
\n", monitorVariables.dns[0], monitorVariables.dns[1], monitorVariables.dns[2], monitorVariables.dns[3]); else sprintf(webPage + strlen(webPage), "DNS Address
\n"); // Now the site latitude & longitude // Convert latitude to char ftoa(s, monitorVariables.latitude, 2); if (monitorVariables.latitude) sprintf(webPage + strlen(webPage), "Site latitude
\n", s); else sprintf(webPage + strlen(webPage), "Site latitude
\n"); ftoa(s, monitorVariables.longitude, 2); if (monitorVariables.longitude) sprintf(webPage + strlen(webPage), "Site longitude
\n", s); else sprintf(webPage + strlen(webPage), "Site longitude
\n"); // Solar panel angle range sprintf(webPage + strlen(webPage), "Solar Panel Minimum Elevation
\n", monitorVariables.minAngle[0]); sprintf(webPage + strlen(webPage), "Solar Panel Maximum Elevation

\n", monitorVariables.maxAngle[0]); sprintf(webPage + strlen(webPage), "Solar Panel Minimum Azimuth bearing
\n", monitorVariables.minAngle[1]); sprintf(webPage + strlen(webPage), "Solar Panel Maximum Azimuth bearing

\n", monitorVariables.maxAngle[1]); // Some commands - tracking, align panels or calibrate sprintf_P(webPage + strlen(webPage), PSTR("Installation Commands

Track Sun
")); sprintf_P(webPage + strlen(webPage), PSTR("Set Panels To minimum position
")); sprintf_P(webPage + strlen(webPage), PSTR("Set Panels To Maximum position
")); sprintf_P(webPage + strlen(webPage), PSTR("Calibrate Panels
")); yield(); sprintf(webPage + strlen(webPage), "
\n"); sprintf(webPage + strlen(webPage), "
"); server.send(200, "text/html", webPage); // Scan for other networks ssids = WiFi.scanNetworks(); } void handleCommand() { static char s[64]; if (server.args() > 0) // got a value { if (!os_strcmp(server.argName(0).c_str(), "level")) // Got a level argument? { server.send(200, "text/plain", "Level Displayed"); } else server.send(200, "text/plain", "You need to send '='"); } } void handleOn() { server.send(200, "text/plain", "LED ON"); } void handleOff() { server.send(200, "text/plain", "LED OFF"); } void handleNotFound() { String message = "File Not Found\n\n"; message += "URI: "; message += server.uri(); message += "\nMethod: "; message += (server.method() == HTTP_GET)?"GET":"POST"; message += "\nArguments: "; message += server.args(); message += "\n"; for (uint8_t i=0; i (oneSecond + 1000)) // Every second { ++loopCount; // See if we have a panel command from the webpage if (panelsToMin) { dispBuffer[0][0] = dispBuffer[1][0] = dispBuffer[2][0] = dispBuffer[3][0] = dispBuffer[4][0] = dispBuffer[5][0] = dispBuffer[6][0] = 0; sprintf_P(dispBuffer[0], PSTR("Panel Alignment")); sprintf_P(dispBuffer[2], PSTR("Elevation to Min")); updateDisplay(); actuatorRunTimer = 0; setActuator(ELEVATION, PANELDOWN); // Elevation to minimum delay(2000); while (panelMoving) yield(); // wait until it stops moving setActuator(ELEVATION, PANELSTOP); actualPanelAngle[ELEVATION] = 0; sprintf_P(dispBuffer[2], PSTR("Azimuth to Min")); updateDisplay(); setActuator(AZIMUTH, PANELDOWN); // Elevation to minimum delay(2000); while (panelMoving) yield(); // wait until it stops moving setActuator(AZIMUTH, PANELSTOP); actualPanelAngle[AZIMUTH] = 0; panelsToMin = 0; // end the command } if (panelsToMax) { dispBuffer[0][0] = dispBuffer[1][0] = dispBuffer[2][0] = dispBuffer[3][0] = dispBuffer[4][0] = dispBuffer[5][0] = dispBuffer[6][0] = 0; sprintf_P(dispBuffer[0], PSTR("Panel Alignment")); sprintf_P(dispBuffer[2], PSTR("Elevation to Max")); updateDisplay(); actuatorRunTimer = 0; setActuator(ELEVATION, PANELUP); // Elevation to minimum delay(2000); while (panelMoving) yield(); // wait until it stops moving setActuator(ELEVATION, PANELSTOP); actualPanelAngle[ELEVATION] = monitorVariables.maxAngle[ELEVATION]; sprintf_P(dispBuffer[2], PSTR("Azimuth to Max")); updateDisplay(); setActuator(AZIMUTH, PANELUP); // Elevation to minimum delay(2000); while (panelMoving) yield(); // wait until it stops moving setActuator(AZIMUTH, PANELSTOP); actualPanelAngle[AZIMUTH] = monitorVariables.maxAngle[AZIMUTH]; panelsToMax = 0; // end the command } switch(displayMode) { case 0: dispBuffer[0][0] = dispBuffer[1][0] = dispBuffer[2][0] = dispBuffer[3][0] = dispBuffer[4][0] = dispBuffer[5][0] = dispBuffer[6][0] = 0; if (toggleIndicator) dispBuffer[0][0] = '*'; else dispBuffer[0][0] = ' '; toggleIndicator = !toggleIndicator; sprintf_P(&dispBuffer[0][1], PSTR("Unixtime %lu"), unixTime); sprintf_P(dispBuffer[1],PSTR("%d:%d:%d %d/%d/%d"), hour(), minute(), second(), day(), month(), year()); sprintf_P(dispBuffer[2],PSTR("Lat=%s,lng=%s"), latitude, longitude); sprintf_P(dispBuffer[3], PSTR("IP %d.%d.%d.%d"), WiFi.localIP()[0], WiFi.localIP()[1], WiFi.localIP()[2], WiFi.localIP()[3]); sprintf_P(dispBuffer[4], PSTR("Elev=%d Azi=%d"), sunElevation, azimuth); // if (!gotGPS) // Display the GPS buffer if we haven't yet got a GPS connection - for debugging // strncpy(dispBuffer[5], gpsBuffer, 23); sprintf_P(dispBuffer[5], PSTR("I=%d"), actuatorCurrent); if (client.connected()) sprintf_P(dispBuffer[6], PSTR("MQTT Connected")); else sprintf_P(dispBuffer[6], PSTR("MQTT Not Connected")); updateDisplay(); break; case 1: dispBuffer[0][0] = dispBuffer[1][0] = dispBuffer[2][0] = dispBuffer[3][0] = dispBuffer[4][0] = dispBuffer[5][0] = dispBuffer[6][0] = 0; sprintf_P(dispBuffer[0], PSTR("Calibrate Panel %d"), calibrate); switch (calibrateState) { case 0: sprintf_P(dispBuffer[2], PSTR("Extend")); break; case 1: sprintf_P(dispBuffer[2], PSTR("Timing Retraction")); break; case 2: sprintf_P(dispBuffer[2], PSTR("Timing Extension")); break; } updateDisplay(); break; } //oled.display(); oneSecond = millis(); setTime(unixTime); if (bootTimer) --bootTimer; // Some Commands // Command to set a panel angle? if (commandSetPanelAngle1) // Non-zero value? { setPanelAngle(0, commandSetPanelAngle1); commandSetPanelAngle1 = 0; // clear the command } if (commandSetPanelAngle2) // Non-zero value? { setPanelAngle(1, commandSetPanelAngle2); commandSetPanelAngle2 = 0; // clear the command } if (commandSetPanelAngle3) // Non-zero value? { setPanelAngle(2, commandSetPanelAngle3); commandSetPanelAngle3 = 0; // clear the command } // Are we in a calibration check? if ((calibrateDelay == 0) && doCalibrate) { sprintf(s, "%d", calibrateState); client.publish(catString(outTopic, "/status/calibrateState"), s); yield(); switch (calibrateState) { case 0: setActuator(calibrate, PANELUP); calibrateState = 1; // Set to move panel to top calibrateDelay = 300; // delay to allow motor speed check to catch up break; case 1: // Check to see if the motor has stopped moving - COULD PROBABLY DO WITH A COUPLE OF MINUTES TIMEOUT HERE JUST IN CASE THERE@S A MOTOR PROBLEM if (panelMoving) break; // Wait until the panel has stopped moving setActuator(calibrate, PANELDOWN); downElevateTimer[calibrate] = 0; doDownElevateTimer = calibrate + 1; // Indicate which panel we are calibrating calibrateState = 2; calibrateDelay = 300; // Slight delay to allow the panel to start moving break; case 2: // Wait until the panel stops moving at the bottom if (panelMoving) // Wait until the panel stops moving break; doDownElevateTimer = 0; // Stop the calibration check monitorVariables.downElevateTimer[calibrate] = downElevateTimer[calibrate]; // Store the panel down time setActuator(calibrate, PANELSTOP); calibrateState = 3; calibrateDelay = 300; // Slight delay to allow the panel to stop properly break; case 3: // Time the UP panel time setActuator(calibrate, PANELUP); upElevateTimer[calibrate] = 0; doUpElevateTimer = calibrate + 1; // Indicate which panel we are calibrating calibrateState = 4; calibrateDelay = 300; // Slight delay to allow the panel to start moving break; case 4: // Wait until the panel stops moving at the top if (panelMoving) // Wait until the panel stops moving break; doUpElevateTimer = 0; // Stop the calibration check monitorVariables.upElevateTimer[calibrate] = upElevateTimer[calibrate]; // Store the panel UP time setActuator(calibrate, PANELSTOP); calibrateState = 5; calibrateDelay = 300; // Slight delay to allow the panel to stop properly break; case 5: // Store the timers into Flash eeprom_save(); calibrateState = 0; // End this calibration client.publish(catString(outTopic, "/status/calibrateState"), "Calibrate Finished"); if (++calibrate > 1) { displayMode = calibrate = doCalibrate = 0; // End calibration sequence } break; } } } // Ten second timer if (millis() > (tenSeconds + 10000)) { tenSeconds = millis(); if (strcmp(monitorVariables.ssid, "xxxx")) // Wifi has been configured { if (!WiFi.isConnected()) // Lost wifi connection - or wifi not initialised? { if (firstWifiCOnnection) { // Initialise the wifi if (monitorVariables.staticIP != IPAddress(0, 0, 0, 0)) WiFi.config(monitorVariables.staticIP, monitorVariables.gateway, monitorVariables.submask); yield(); WiFi.begin(monitorVariables.ssid, monitorVariables.ssidPassword); // Initialise the wifi if this is the first attempt firstWifiCOnnection = 0; } else { WiFi.reconnect(); // Otherwise attempt a reconnection yield(); } } } if (WiFi.isConnected()) { if (useMQTT && !client.connected()) // make sure that we have an MQQT connection { // Attempt reconnection client.connect(mqttID, monitorVariables.mqttUser, monitorVariables.mqttPassword); yield(); if (client.connected()) // make sure that we have an MQTT connection { sprintf_P(s,PSTR("{\"type\":\"Solar Tracker\",\"deviceID\":\"%s\"}"), monitorVariables.mqttDevice); client.publish(outTopic, s); sprintf_P(s,PSTR("{\"id\":\"%s\",\"desc\":\"none\",\"attr\":\"none\",\"fail\":0}"), monitorVariables.mqttDevice); client.publish("esplogon", s); // Re-logon to the MQTT server yield(); client.subscribe(catString(inTopic, "/#")); // Make sure that we are subscribed to all relevant topics sprintf_P(s, PSTR("MQTT %s "), catString(inTopic, "/#")); } } } if (useMQTT) if (client.connected()) // make sure that we have an MQQT connection { yield(); if (justBooted && (bootTimer == 0) && unixTime) { // Publish boot time sprintf(s,"Version %s. Reboot Time %02d:%02d:%02d ", VER, hour(), minute(), second()); client.publish(catString(outTopic, "/version"), s); yield(); sprintf(s,"{\"id\":\"%s\",\"desc\":\"Solar Tracker\",\"attribute\":\"NONE\",\"fail\":\"0\"}", mqttID); client.publish("esplogon", s); yield(); justBooted = 0; } } if (gps.location.isUpdated()) { int t[10]; int locTime; yield(); gotGPS = 1; // Have got a GPS signal sprintf_P(dispBuffer[5], PSTR("GPS OK")); monitorVariables.latitude= gps.location.lat(); monitorVariables.longitude = gps.location.lng(); // Work out the time zone myTimeZone = (int) (monitorVariables.latitude / 15.0L); //will be 0-23 ftoa(latitude, monitorVariables.latitude, 2); ftoa(longitude, monitorVariables.longitude, 2); tYear = gps.date.year(); tMonth = gps.date.month(); tDay = gps.date.day(); tHour = gps.time.hour(); // Calculate the local (non DST) time by using the longitude locTime = ((int) monitorVariables.longitude) / 15; // +/- 12 if (locTime >= 0 ) // positive time zone tHour = (tHour + locTime) % 24; // make sure it can't be more than 23 else { // negative time zone if (abs(locTime) > tHour) // NOT BOTHERED ABOUT ADJUSTING THE DATE AT THE MOMENT tHour = (24 + tHour + locTime) % 24; // locTime is negative and greater than tHour else tHour = (tHour + locTime) % 24; // loctime is negative, but less than tHour } tMinute = gps.time.minute(); tSecond = gps.time.second(); // Adjust from UTC to a local (non DST) time by dividing the longitude by 24 and using as an offset unixTime = tmConvert_t(tYear, tMonth, tDay, tHour, tMinute, tSecond); setTime(unixTime); } } if (millis() > (oneMinute + 60000)) { oneMinute= millis(); // sprintf(s,"\r\nLat=%s, long=%s (%02d:%02d, %02d/%02d/%02d - %lu)\r\n\n", latitude, longitude, hour(), minute(), day(), month(), year(), unixTime); // Serial.print(s); if (useMQTT && client.connected()) { if (unixTime == 0) // Time not set yet, might as well try to get it from the server instead of the GPS { sprintf_P(s,PSTR("{\"id\":\"%s\",\"desc\":\"none\",\"attr\":\"none\",\"fail\":0}"), monitorVariables.mqttDevice); client.publish("esplogon", s); // Re-logon to the MQTT server yield(); client.loop(); // Handle MQTT yield(); } sprintf_P(s,PSTR("{\"heartBeat\":\"%02d:%02d:%02d\",\"heartBeatU\":\"%ld\"}"), hour(), minute(), second(), unixTime); client.publish(catString(outTopic, "/status"), s); yield(); client.loop(); // Handle MQTT yield(); } // Check the panel angle and adjust it according to the required solar angle if (unixTime && tracking) // Make sure that the time has been updated { int l = (int) monitorVariables.latitude; setTime(unixTime); // Make sure that the time has been properly set sunElevation = solar_elevation(l, calculateDayOfYear(day(), month(), year()), (hour() * 60) + minute()); // Now work out if we need to move the panels if (!doCalibrate) { // Only move the panels if the sun is up and there has been more than a 5 degree change - avoids too many cumulative angle errors if ((abs(actualPanelAngle[ELEVATION] + monitorVariables.minAngle[ELEVATION] - (90 - sunElevation)) >= 5) && (sunElevation > 0)) setPanelAngle(ELEVATION, 90 - sunElevation - monitorVariables.minAngle[ELEVATION]); // Set the current panel elevation if (sunElevation > 0) // Sun above the horizon? { float az, sunAz; float hr, mn; if (sunsetTimer == -2) // have previously done a sunset panel zero, so prime things ready for the next evening sunsetTimer = -1; // convert unix time to hours and minutes into the day hr = (myTimeZone + ((unixTime % SECONDS_IN_DAY) / 3600)) % 24; // Hour of the day - adjusted for longitude mn = ((unixTime % SECONDS_IN_DAY) / 60) % 60; // Minute of day sunAz = (15L * (hr + (mn / 60))); azimuth = (int) sunAz; if ((azimuth > monitorVariables.minAngle[1]) && (azimuth < monitorVariables.maxAngle[1])) { int theta = azimuth - monitorVariables.minAngle[1]; // Translate the bearing into 0-maxAngle if (abs(theta - actualPanelAngle[AZIMUTH]) >= 5) // Adjust angle if the movement is 5 degrees or more setPanelAngle(AZIMUTH, theta); } } else { // Is now past sunset if (sunsetTimer == -1) // Not already timing until dark sunsetTimer = 60 * 30; // 30 minutes post sunset before we rotatethe panels to the zero position if (sunsetTimer == 0) // 30 mins after dark, so zero the panels { azimuth = 0; // Clear the display for (int y =0; y < 7; y++) for (int x = 0; x < 23; x++) dispBuffer[y][x] = ' '; // Now set the linear actuators to their start position for tomorrow // Move the panels to the reference point = minimum solar angle - i.e. max extension of the actuator sprintf_P(dispBuffer[0], PSTR("Initialising Panels")); sprintf_P(dispBuffer[2], PSTR("Elevation to Max")); updateDisplay(); // Set the inital panel angles actuatorRunTimer = 0; setActuator(ELEVATION, PANELUP); // Elevation angle delay(2000); while (panelMoving) yield(); // wait until it stops moving setActuator(ELEVATION, PANELSTOP); actualPanelAngle[ELEVATION] = monitorVariables.maxAngle[ELEVATION] - monitorVariables.minAngle[ELEVATION]; sprintf_P(dispBuffer[3], PSTR("Azimuth to Zero")); updateDisplay(); setActuator(AZIMUTH, PANELUP); // Azimuth delay(2000); while (panelMoving) yield(); // wait until it stops moving setActuator(AZIMUTH, PANELSTOP); actualPanelAngle[AZIMUTH] = 0; sprintf_P(dispBuffer[4], PSTR("Panels Zeroed")); updateDisplay(); delay(2000); sunsetTimer = -2; // Indicates that the panels are in the night time position } } sprintf(s,"{\"elevation\":%d,\"azimuth\":%d}", sunElevation, azimuth); client.publish(outTopic, s); } } } // Ten minute timer if (millis() > (tenMinutes + (60000 * 10))) { tenMinutes = millis(); } } } // Concatenate 2 strings - max result is 256 bytes char * catString (char *s1, char *s2) { static char result[256]; sprintf(result, "%s%s", s1, s2); return (result); } // Store the contents of the variable structure to FLASH void eeprom_save(void) { byte *ep = (byte *) &monitorVariables; for (int i = 0; i < sizeof(monitorVariables); i++) { EEPROM.write(i, *ep++); yield(); } EEPROM.commit(); } // Float to ASCII conversion char *ftoa(char *a, double f, int precision) { long p[] = {0,10,100,1000,10000,100000,1000000,10000000,100000000}; char *ret = a; long heiltal = (long)f; itoa(heiltal, a, 10); while (*a != '\0') a++; *a++ = '.'; long desimal = abs((long)((f - heiltal) * p[precision])); itoa(desimal, a, 10); return ret; } // Convert day of year, latitude and time to solar elevation angle in degrees // dayOfYear is an integer, timeOfDay is an integer in minutes from midnight (0) static float radians = 3.14159L / 180L; int solar_elevation(float latitude, int dayOfYear, int timeOfDay) { static float decA, declinationAngle; static float angle; static float floatTime; static float hourAngle; int A; static char s[32]; floatTime = ((float) timeOfDay) / 60L; hourAngle = (floatTime - 12L) * 15L; decA = ((radians * 360L) /365L) * ((float) dayOfYear + 10L); declinationAngle = -23.45 * cos(decA); angle = asin( (sin(radians * latitude) * sin(radians * declinationAngle)) + (cos(radians * latitude) * cos(radians * declinationAngle) * cos(radians * hourAngle))) / radians; if (angle >= 0) angle += 0.5L; // rounded to nearest integer value else angle -= 0.5L; // rounded to nearest integer value A = (int) angle; return (A); } // Calculate sunset/sunrise // SPEEDED THIS UP BY DOING AN APPROXIMATION THEN A DETAILED SEARCH void sunsetToSunrise(int dayOfYear, int *sunrise, int *sunset) { bool gotSunrise, gotSunset; gotSunset = 0; gotSunrise = 0; // Scan in in increments of 30 minutes and then backtrack to find exact minute of sunset/sunrise. This speeds up the search for (int timeOfDay = 0; timeOfDay < 24*60; timeOfDay+= 30) // Scan the hours from 0 AM til 12 midnight in minutes { yield(); if (!gotSunrise) // Looking for sunrise if (solar_elevation(monitorVariables.latitude, dayOfYear, timeOfDay) >= 0) { for (int tod = (timeOfDay - 30); tod < timeOfDay; tod++) // Scan from 30 minutes previously to current scan time if (solar_elevation(monitorVariables.latitude, dayOfYear, tod) >= 0) { // Found sunrise *sunrise = tod; gotSunrise = 1; break; } } if (!gotSunset && gotSunrise) // Looking for sunset if (solar_elevation(monitorVariables.latitude, dayOfYear, timeOfDay) < 0) { for (int tod = (timeOfDay - 30); tod < timeOfDay; tod++) // Scan from 30 minutes previously to current scan time if (solar_elevation(monitorVariables.latitude, dayOfYear, tod) < 0) { *sunset = tod; gotSunset = 1; // Not really needed! break; } } if (gotSunset) break; // Finish when we have found the sunset } } // Convert panel angle to linear actuator extension length int angleToActuatorLength(int theta) { return (sqrt(sq(monitorVariables.actuatorAttachmentHeight) + sq(monitorVariables.actuatorBaseLength) - (2 * monitorVariables.actuatorAttachmentHeight * monitorVariables.actuatorBaseLength * cos((theta) * radians)))); } // Set the actuator moving or stopping - 0 = STOP, -1 = DOWN, +1 = UP void setActuator (int whichActuator, char actStatus) { yield(); currentActuator = whichActuator; switch (actStatus) { case PANELSTOP: // STOP actuatorStatus = PANELSTOP; break; case PANELUP: // UP if (actuatorStatus == PANELDOWN) // Was it going in the opposite direction? { actuatorStatus = PANELSTOP; while (actuatorSpeed) yield(); // Wait until actuator has stopped } actuatorStatus = PANELUP; break; case PANELDOWN: // DOWN if (actuatorStatus == PANELUP) // Was it going in the opposite direction? { actuatorStatus = PANELSTOP; while (actuatorSpeed) yield(); // Wait until actuator has stopped } actuatorStatus = PANELDOWN; break; } } // Set the panel angle - assume linear conversion between angles and actuator extension /* int setPanelAngle(int whichPanel, int theta) { long int distanceToRun, dTime, eLength; if (theta == actualPanelAngle[whichPanel - 1]) // Don't do anything if the panel is in the right position return(0); // Angle range check if (theta > monitorVariables.maxAngle[whichPanel - 1]) theta = monitorVariables.maxAngle[whichPanel - 1]; if (theta < monitorVariables.minAngle[whichPanel - 1]) theta = monitorVariables.minAngle[whichPanel - 1]; distanceToRun = (long) ((angleToActuatorLength(theta) - angleToActuatorLength(actualPanelAngle[whichPanel - 1]))); // could be +/- eLength = (long) (maxExtensionLength - minExtensionLength); // range of actuator movement if (distanceToRun < 0) // Panel needs to move down { dTime = (long) monitorVariables.downElevateTimer[whichPanel - 1]; setActuator(whichPanel, PANELDOWN); actuatorRunTimer = (int) (((abs(distanceToRun) * dTime * 100) / eLength) + 100); // Number of 100th of seconds to run - add on 100 for a 1 second speed up/slow down } else { dTime = (long) monitorVariables.upElevateTimer[whichPanel - 1]; setActuator(whichPanel, PANELUP); actuatorRunTimer = ((abs(distanceToRun) * dTime * 100) / eLength) + 100; // Number of 100th of seconds to run - add on 100 for a 1 second speed up/slow down } while (actuatorRunTimer) // wait until the panel move has finished { yield(); client.loop(); } actualPanelAngle[whichPanel - 1] = theta; return(theta); } */ // Set panel number (0-2) to angle theta int setPanelAngle(int whichPanel, int theta) { int deltaTheta; float dTime, degreesPerSecond; yield(); // Invert the panel angle for the azimuth tracker as the tracker linear actuator is on the wrong side if (whichPanel == AZIMUTH) theta = monitorVariables.maxAngle[AZIMUTH] + monitorVariables.minAngle[AZIMUTH] - theta; if (theta == actualPanelAngle[whichPanel]) // Don't do anything if the panel is in the right position return(0); // Angle range check if (theta > monitorVariables.maxAngle[whichPanel]) theta = monitorVariables.maxAngle[whichPanel]; if (theta < monitorVariables.minAngle[whichPanel]) theta = monitorVariables.minAngle[whichPanel]; deltaTheta = theta - actualPanelAngle[whichPanel]; // Angle differential if (deltaTheta < 0) // linear actuator needs to be retracted { degreesPerSecond = (((monitorVariables.maxAngle[whichPanel] - monitorVariables.minAngle[whichPanel])) / monitorVariables.downElevateTimer[whichPanel]); // muliplied by 100 for 1/100s of a second interval dTime = abs(deltaTheta) / ((int) degreesPerSecond); // Number of 1/100 seconds to run actuatorRunTimer = (int) ((dTime * 100) + 100); // Number of 100th of seconds to run - add on 100 for a 1 second speed up/slow down setActuator(whichPanel, PANELDOWN); } else { degreesPerSecond = (((monitorVariables.maxAngle[whichPanel] - monitorVariables.minAngle[whichPanel])) / monitorVariables.upElevateTimer[whichPanel]); // muliplied by 100 for 1/100s of a second interval dTime = abs(deltaTheta) / ((int) degreesPerSecond); // Number of 1/100 seconds to run actuatorRunTimer = (int) ((dTime * 100) + 100); // Number of 100th of seconds to run - add on 100 for a 1 second speed up/slow down setActuator(whichPanel, PANELUP); } while (actuatorRunTimer) // wait until the panel move has finished { server.handleClient(); // Handle local web server yield(); if (useMQTT) client.loop(); // Handle MQTT yield(); } actualPanelAngle[whichPanel] = theta; return(deltaTheta); // Return angle to turn } time_t tmConvert_t(int YYYY, byte MM, byte DD, byte hh, byte mm, byte ss) { tmElements_t tmSet; tmSet.Year = YYYY - 1970; tmSet.Month = MM; tmSet.Day = DD; tmSet.Hour = hh; tmSet.Minute = mm; tmSet.Second = ss; return makeTime(tmSet); } //////////////////////////////////////////////// // Calculate day of year from Jan 1 frmo Date int calculateDayOfYear(int day, int month, int year) { // Given a day, month, and year (4 digit), returns // the day of year. Errors return 999. int daysInMonth[] = {31,28,31,30,31,30,31,31,30,31,30,31}; // Verify we got a 4-digit year if (year < 1000) { return 999; } // Check if it is a leap year, this is confusing business // See: https://support.microsoft.com/en-us/kb/214019 if (year%4 == 0) { if (year%100 != 0) { daysInMonth[1] = 29; } else { if (year%400 == 0) { daysInMonth[1] = 29; } } } // Make sure we are on a valid day of the month if (day < 1) { return 999; } else if (day > daysInMonth[month-1]) { return 999; } int doy = 0; for (int i = 0; i < month - 1; i++) { doy += daysInMonth[i]; } doy += day; return doy; } void updateDisplay() { int count = 21 * 8; // Number of chars to display int x, y; bool skipOut; oled.clearDisplay(); oled.setCursor(0, 0); for (y = 0; y < 7; y++) { skipOut = 0; oled.setCursor(0, y * 8); for (x = 0; x < 21; x++) { if (dispBuffer[y][x] != 0) oled.print(dispBuffer[y][x]); else // Put spaces for the rest of the line { for (int x1 = x; x1 < 21; x1++) oled.print(' '); skipOut = 1; } if (skipOut) break; } } oled.display(); }