#include <LiquidCrystal_I2C.h>
#include <WiFi.h>
#include <AiEsp32RotaryEncoder.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <ESP32Servo.h>
#include <ESP32Time.h>
// Defining used structs
struct AzTriplet {
long time;
float azimute;
float altitude;
};
struct PositionData {
AzTriplet start;
AzTriplet Max;
AzTriplet End;
};
// Function Headers
void ModifyScreen(bool);
void GetSatelliteData(int, float, float, float, int, int, int);
void DrawScreen(void);
void FillMenuData(void);
void SelectScreen(void);
void Calibrate(void);
void DisplayGPS(void);
void GetWIFI(void);
void FollowSatellite(int, int);
// Creating the matrix to store the mposition value pairs
PositionData PositionMatrix[3][4];
// Global Menu Data structure
String MenuItems[][5] = {
// Home menu
{"NOAA 15", "NOAA 18", "NOAA 19", "Settings", ""},
// Placeholders fro online query
{"Exit", "", "", "", ""},
{"Exit", "", "", "", ""},
{"Exit", "", "", "", ""},
// Settings
{"Exit", "Calibrate", "Get GPS", "Change Wifi", "Update Keplers"}
};
// In order to keep track the number of menu items
int MenuItemsNumbers[] = {4, 5l , 5, 5, 5};
//Define what screen is being displayed
int CurrentScreen = 0;
// MENU controll variables
int ArrowPosition = 0;
bool increment;
int MenuStart = 0;
// Creating an array of NORAD numbers
int NORADnums[] = {25338, 28654, 33591};
// Rotary encoder setup
#define ROTARY_ENCODER_A_PIN 12
#define ROTARY_ENCODER_B_PIN 14
#define ROTARY_ENCODER_BUTTON_PIN 13
#define ROTARY_ENCODER_VCC_PIN -1
#define ROTARY_ENCODER_STEPS 4
AiEsp32RotaryEncoder rotaryEncoder = AiEsp32RotaryEncoder(ROTARY_ENCODER_A_PIN, ROTARY_ENCODER_B_PIN, ROTARY_ENCODER_BUTTON_PIN, ROTARY_ENCODER_VCC_PIN, ROTARY_ENCODER_STEPS);
int EncoderValue = 0;
void IRAM_ATTR readEncoderISR()
{
rotaryEncoder.readEncoder_ISR();
}
// Defining API key
const String APIkey = "EP5Y42-UMYJR6-ZUKX2G-508T";
// set the LCD number of columns and rows
int lcdColumns = 20;
int lcdRows = 4;
// set LCD address, number of columns and rows
// if you don't know your display address, run an I2C scanner sketch
LiquidCrystal_I2C lcd(0x27, lcdColumns, lcdRows);
// Defining servos
Servo AzimutalServo;
Servo AltitudeServo;
int StepN = 100;
// Defines a gear ration of 1:8 for the azimutal servo
const int GearRatio = 8;
// Defining the timezone variables
const char* ntpServer = "pool.ntp.org";
const long gmtOffset_sec = 0;
const int daylightOffset_sec = 0;
// Variables to tranform the epoch time into a readable value
struct tm timeinfo;
void setup(){
// LCD initialization
// initialize LCD
lcd.init();
// turn on LCD backlight
lcd.backlight();
lcd.setCursor(0, 0);
lcd.print("loading...");
// Wifi initialization
// Starts the serial port
Serial.begin(9600);
Serial.print("Connecting to WiFi");
WiFi.begin("Wokwi-GUEST", "", 6);
while (WiFi.status() != WL_CONNECTED) {
delay(100);
Serial.print(".");
}
Serial.println(" Connected!");
// Filling the menu up
FillMenuData();
// Starting servos
AltitudeServo.attach(27);
AzimutalServo.attach(15);
Calibrate();
// Drawing the initial screen
DrawScreen();
lcd.setCursor(0, 0);
lcd.print(">");
//Disabling acceleration for encoder
rotaryEncoder.begin();
rotaryEncoder.setup(readEncoderISR);
rotaryEncoder.disableAcceleration();
// Initializing time
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
}
void loop(){
// Waiting for rotary encoder to change
if (rotaryEncoder.encoderChanged()) {
// Checking if the value was incremented or decremented
if (EncoderValue < rotaryEncoder.readEncoder()) {
increment = true;
} else {
increment = false;
}
// Changing arrow position
EncoderValue = rotaryEncoder.readEncoder();
ModifyScreen(increment);
}
// Waiting for the button to be pressed
if (rotaryEncoder.isEncoderButtonClicked()) {
// Checking if in the main screen
if (CurrentScreen == 0) {
CurrentScreen = ArrowPosition + MenuStart + 1;
SelectScreen();
// Describing what happens in the Settings screen
} else if (CurrentScreen == 4) {
switch (ArrowPosition + MenuStart) {
// Exit
case 0:
CurrentScreen = 0;
SelectScreen();
break;
// Calibrate
case 1:
Calibrate();
break;
// Displaying GPS coordinates
case 2:
DisplayGPS();
break;
// Changing the wifi password
case 3:
GetWIFI();
break;
// Update keplers
case 4:
lcd.clear();
lcd.print("loading...");
FillMenuData();
CurrentScreen = 0;
SelectScreen();
break;
}
// Selecting what happens in the satellite reception screens
} else {
if (ArrowPosition + MenuStart == 0) {
CurrentScreen = 0;
SelectScreen();
} else {
FollowSatellite(CurrentScreen - 1, ArrowPosition + MenuStart - 1);
}
}
}
}
void SelectScreen (void) {
// Drawin the page
MenuStart = 0;
ArrowPosition = 0;
DrawScreen();
// Printing new cursor
lcd.setCursor(0, ArrowPosition);
lcd.print(">");
}
void ModifyScreen(bool increment){
// Clearing old arrow
lcd.setCursor(0, ArrowPosition);
lcd.print(" ");
// Cheching if it is an increment or decrement that has been made
if (increment) {
if (ArrowPosition != 3) {
ArrowPosition++;
}
else if (MenuStart != MenuItemsNumbers[CurrentScreen] - 4) {
MenuStart++;
DrawScreen();
}
} else {
if (ArrowPosition != 0) {
ArrowPosition--;
} else if (MenuStart != 0) {
MenuStart--;
DrawScreen();
}
}
// Printing new cursor
lcd.setCursor(0, ArrowPosition);
lcd.print(">");
}
void DrawScreen() {
// Resetting the screen
lcd.clear();
// Drawing the new content
for (int i = 0; i < 4; i++) {
lcd.setCursor(1, i);
lcd.print(MenuItems[CurrentScreen][MenuStart + i]);
}
}
void FillMenuData() {
// Making getting data for the different NORAD id's
for (int i = 0; i < 3; i++) {
GetSatelliteData(NORADnums[i], 0, 0, 0, 5, 40, i);
}
};
void GetSatelliteData(int id, float lat, float lng, float alt, int days, int minElev, int NoradId) {
// Initializing client
HTTPClient http;
// Creating request string
String request = "https://api.n2yo.com/rest/v1/satellite/radiopasses/" + String(id) + "/" + String(lat) + "/" + String(lng) + "/" + String(alt) + "/" + String(days) + "/" + String(minElev) + "&apiKey=" + APIkey;
// Making the requent
http.begin(request);
http.GET();
// Recieving the response
String response = http.getString();
// Initializing JSON
DynamicJsonDocument JSON(2048);
// Parsing JSON string
DeserializationError error = deserializeJson(JSON, response);
if (error) {
Serial.print("deserializeJson() failed: ");
Serial.println(error.c_str());
return;
};
// Checking of how many passes
int PassesNum = JSON["passes"].size();
if (PassesNum < MenuItemsNumbers[NoradId + 1] - 1) {
MenuItemsNumbers[NoradId + 1] = PassesNum + 1;
};
// Defining counter
int j = 0;
// Iterating over every element of the json
for (JsonObject passe : JSON["passes"].as<JsonArray>()) {
// Filling th position matrix up with starting values
PositionMatrix[NoradId][j].start.time = passe["startUTC"];
PositionMatrix[NoradId][j].start.azimute = passe["startAz"];
PositionMatrix[NoradId][j].start.altitude = 0;
// Filling the position matrix up with its max values
PositionMatrix[NoradId][j].Max.time = passe["maxUTC"];
PositionMatrix[NoradId][j].Max.azimute = passe["maxAz"];
PositionMatrix[NoradId][j].Max.altitude = passe["maxEl"];
// Filling the position matrix up with its ending values
PositionMatrix[NoradId][j].End.time = passe["endUTC"];
PositionMatrix[NoradId][j].End.azimute = passe["endAz"];
PositionMatrix[NoradId][j].End.altitude = 0;
// Just a test
time_t Epoch = passe["startUTC"];
String FullEpochString = ctime(&Epoch);
// Converting string to integer
char buf[50];
long MaxAlt = passe["maxEl"];
ltoa(MaxAlt, buf, 10);
// TEST DEVICE -> This should be removed afterwards
Serial.println(FullEpochString.substring(8,11) + FullEpochString.substring(4,8) + FullEpochString.substring(11,20) + MaxAlt + "d");
// Ading date to the menu
MenuItems[NoradId + 1][j + 1] = FullEpochString.substring(8,11) + FullEpochString.substring(4,8) + FullEpochString.substring(11,20) + MaxAlt + "d"; // 1521451295, 1521600275
if (j > 2 || j >= PassesNum)
break;
j++;
};
Serial.println("\n");
// Closing the http client
http.end();
}
void Calibrate(void) {
AzimutalServo.write(0);
AltitudeServo.write(0);
return;
}
void DisplayGPS(void) {
return;
}
void GetWIFI(void) {
return;
}
void FollowSatellite(int SatNum, int pass) {
// Calculating the wait time between steps
long FirstDelay = (PositionMatrix[SatNum][pass].Max.time - PositionMatrix[SatNum][pass].start.time)/StepN * 1000;
long SecondDelay = (PositionMatrix[SatNum][pass].End.time - PositionMatrix[SatNum][pass].Max.time)/StepN * 1000;
// Calculating the angular step
float AltitudeIncr = (PositionMatrix[SatNum][pass].Max.altitude - PositionMatrix[SatNum][pass].start.altitude)/StepN;
float AltitudeDecr = (PositionMatrix[SatNum][pass].End.altitude - PositionMatrix[SatNum][pass].Max.altitude)/StepN;
// Calculating the azimutal step
float AzimutalIncr = ((PositionMatrix[SatNum][pass].Max.azimute - PositionMatrix[SatNum][pass].start.azimute)/StepN)/GearRatio;
float AzimutalDecr = ((PositionMatrix[SatNum][pass].End.azimute - PositionMatrix[SatNum][pass].Max.azimute)/StepN)/GearRatio;
// Defining a starting altitude and azimute variables
float StartAltitude = PositionMatrix[SatNum][pass].start.altitude;
float StartAzimute = PositionMatrix[SatNum][pass].start.azimute/GearRatio;
// Defining the max altitude and azimute variable
float MaxtAltitude = PositionMatrix[SatNum][pass].Max.altitude;
float MaxtAzimute = PositionMatrix[SatNum][pass].Max.azimute/GearRatio;
// Sweeping the first half of the altitude positions
for (int i = 0; i < StepN; i++) {
AltitudeServo.write(StartAltitude + i*AltitudeIncr);
AzimutalServo.write(StartAzimute + i*AzimutalIncr);
delay(FirstDelay);
Serial.println(String(StartAltitude + i*AltitudeIncr) + "||" + String(StartAzimute + i*AzimutalIncr));
}
// Sweeping the first half of the altitude positions
for (int i = 0; i < StepN; i++) {
AltitudeServo.write(MaxtAltitude + i*AltitudeDecr);
AzimutalServo.write(MaxtAzimute + i*AzimutalDecr);
delay(SecondDelay);
Serial.println(MaxtAltitude + i*AltitudeDecr);
}
return;
}