Code
Enrolled students will be able to copy or download code and the required asset files.
Receive Flight Parameters (Arduino C)
receive_flight_parameters.ino
/*
Project: Receiver for Flight tracker
Project Description:
This sketch sends receiving data from the transmitter via LoRa
This sketch involves readings from GPS , BMP , MPU sensors and is also responsible to send it to processing app.
Author: STEMVentor Educonsulting
This code is copyrighted. Please do not reuse or share for any purpose other than for learning with subscribed STEMVentor programs.
This code is for educational purposes only and is not for production use.
*/
// LIBRARIES
//to transmit data using sx1278 LoRa module
#include <SPI.h>
#include <LoRa.h>
#include <Wire.h> //used for communicating with I2C devices
// install the adafruit ssd1306 library
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
// PIN DEFINITIONS
// No need to physically connect these pins in this use case but need to be defined for the LoRa to transmit
#define ss 15 //LoRa module NSS pin
#define rst 16 //LoRa module RST pin
#define dio0 4 //LoRa module DIO0 pin
// GLOBAL VARIABLES
String Lat;
String Long;
String Temperature;
String Altitude;
String Speed;
String data = ""; //for the incoming data
// Define the display size
const byte SCREEN_WIDTH = 128;// OLED display width, in pixels
const byte SCREEN_HEIGHT = 64;// OLED display height, in pixels
bool check = false;
bool data1_check = false;
bool data2_check = false;
// Timing variables
unsigned long lastUpdateTime = 0; // Tracks the last update time
const unsigned long updateInterval = 5000; // 5 seconds interval
/* INITIALIZE OBJECTS
* Libraries usually follow an object-oriented approach that requires
* an instance of the class to call its methods.
*/
/*
* All I2C components have an address, the default is usually 0x27
* If that doesn't work, see this:https://playground.arduino.cc/Main/I2cScanner/
* The init statement accepts the address and the number of columns and rows.
*/
// Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)
Adafruit_SSD1306 oled(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1); //-1 indicates that the library should not use a hardware reset pin
//Function to receive and serial print the incoming data packet
//Serial printing the data will make it available/readable for the processing app via the comm port.
void onReceive(int packetSize)
{
if (packetSize == 0) return; // if there's no packet, return
data = "";
while (LoRa.available()) {
data += (char)LoRa.read();
}
Serial.println(data);
}
//to display data every 5 seconds onto the OLED
void seggregateData()
{
char dataType = data[0];
data = data.substring(2); // Remove the identifier and the comma
// Split the remaining data by commas
int firstComma = data.indexOf(',');
int secondComma = data.indexOf(',', firstComma + 1);
switch (dataType) {
case 'a': {
float altitude_value = data.substring(0, firstComma).toFloat();
float temp = data.substring(firstComma + 1).toFloat();
Altitude = "Altitude:";
Altitude+= String(altitude_value);
Altitude+= " m";
Temperature = " Temperature:";
Temperature+= String(temp);
Temperature+=" deg C";
data1_check = true;
break;
}
case 'b': {
float latitude = data.substring(0, firstComma).toFloat();
float longitude = data.substring(firstComma + 1, secondComma).toFloat();
float speed_value = data.substring(secondComma + 1).toFloat();
Lat = "Lat:";
Lat+= String(latitude);
Long= " Long:";
Long+= String(longitude);
Speed= " Speed:";
Speed+= String(speed_value);
Speed+=" Km/hr";
data2_check = true;
break;
}
default:
Serial.println("Unknown data type");
break;
}
if (data1_check == true && data2_check == true )
{
check = true;
}
}
//setup function for OLED
void OLED_setup() {
// Initializing wire
Wire.begin();
if(!oled.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3D for 128x64
Serial.println(F("SSD1306 allocation failed"));
}
oled.clearDisplay(); // clear display
oled.setTextSize(1); // set text size
oled.setTextColor(WHITE); // set text color
}
//setup function for LoRa
void lora_setup()
{
Serial.println("Starting LoRa Communication");
LoRa.setPins(ss, rst, dio0); //These pins need not be connected but have to be defined
if (!LoRa.begin(433E6)) { //starts LoRa at 433MHz frequency
Serial.println("Starting LoRa failed!");
while (1); // runs only once
}
// LoRa.setSyncWord(0xFF); // isolates network , the receiver also needs to have the same syncword. More different the syncwords , better the isolation.
}
void setup()
{
// Start the serial communication with baud rate suitable for your components.
Serial.begin(9600);
while (!Serial);
lora_setup();
OLED_setup();
}
void loop()
{
onReceive(LoRa.parsePacket()); //constantly listen for incoming data
unsigned long currentTime = millis(); //checks if 5 seconds have passed by since the last update
if (currentTime - lastUpdateTime >= updateInterval)
{
// Update the OLED with the data
seggregateData();
if (check == true)
{
oled.clearDisplay(); // clear display
oled.setCursor(0, 0); // set position to display (x,y)
oled.println(Lat); // set text
oled.setCursor(0, 12); // set position to display (x,y)
oled.println(Long); // set text
oled.setCursor(0, 24); // set position to display (x,y)
oled.println(Temperature); // set text
oled.setCursor(0, 36); // set position to display (x,y)
oled.println(Altitude); // set text
oled.setCursor(0, 48); // set position to display (x,y)
oled.println(Speed); // set text
oled.display(); // display on OLED
data1_check = false;
data2_check = false;
check = false;
lastUpdateTime = currentTime; // Update the last update time
}
}
}
Display Flight Parameters (Processing)
⚠️
You will need to download and install the processing software from the Processing[.org] website.
display_flight_parameters.pde
import processing.serial.*;
/* Flags to include / exclude code snippets */
boolean TEST_MODE = false; //set to true to use test data for animation of gauge needles
boolean ARDUINO_MODE = true; //set to true to use sensor data from Arduino
boolean API_MODE = false; //set to true to use sensor data from API (ThingSpeak)
//Values for screen layout
int view_width = 1200; //My Laptop's screen width
int view_height = 600; //My Laptop's screen height
int num_rows = 2;
int num_cols = 3;
String ws;
int offset_x = 5;
int offset_y = 5;
int header_width = view_width - (2 * offset_x); //1200 - (2 * 5) = 1190
int header_height = 50; //fixed
int panel_width = (header_width - (2 * offset_x))/num_cols; // 1190 - (2 * 5) = 1180/3 = 393
int panel_height = (view_height-header_height - (4 * offset_y))/num_rows;
float gauge_dia=panel_width*0.6; // 393*0.6 = 236
float SpanAngle=120;
int NumberOfScaleMajorDivisions;
int NumberOfScaleMinorDivisions;
PVector v1, v2;
//Panel and Header center points.
//Calculate the center point for each panel. Draw everything in a panel with respect to its center point
int [][] panel_center_x = new int[num_rows][num_cols]; //2 rows and 3 cols
int [][] panel_center_y = new int[num_rows][num_cols]; //2 rows and 3 cols
int header_center_x = 0;
int header_center_y = 0;
PFont font;
//Sensor inputs
float pitch = 0;
float attitude = 0;
float altitude = 0;
float altitudem = 0;
float temperature = 0;
//float airspeed = 0;
float heading = 0;
float vertical_speed = 0;
float Azimuth;
Serial port;
float Phi; //Dimensional axis
float Theta;
float Psi;
float x;
float y ;
float z;
float latitude; //Dimensional axis
float longitude;
float airspeed;
//For testing code
int draw_counter; //Used for simulating input values from an array with every draw loop reading the next value in the input array
int direction; //Used to run through the input array backwards
//void settings() {
// //fullScreen();
// size(view_width, view_height);
//}
void setup()
{
smooth();
size(1200, 600);
strokeCap(SQUARE);//Optional
//Set all modes to CENTER
rectMode(CENTER);
imageMode(CENTER);
ellipseMode(CENTER);
// The font must be located in the sketch's
// "data" directory to load successfully
font = loadFont("Tahoma-18.vlw");
//Primarily for running through test data
draw_counter=0; //reset to zero everytime the sketch restarts
direction = 1;
if(ARDUINO_MODE){
//println(Serial.list()); //Shows your connected serial ports //-- With Arduino
try {
port = new Serial(this, "COM9", 9600);
print("done");
port.bufferUntil('\n');
} catch (Exception e) {
println("Error: Could not open port " + "COM9");
e.printStackTrace();
}
}
}
/*
* Call this method in setup() to draw the static backdrop for the panels. This will reduce the flicker during animation.
*/
void drawBackdrop(){
background(255);
//Draw Header Panel
fill(0);
//Specifying coordinates directly, without translate
header_center_x = offset_x+header_width/2;
header_center_y = offset_y+header_height/2;
rect(header_center_x, header_center_y, header_width, header_height);
//Draw panels - 2 rows x 3 cols
for(int i=0;i<num_rows;i++){
for(int j=0;j<num_cols;j++){
panel_center_x[i][j] = j*panel_width+panel_width/2+(j+1)*offset_x;
panel_center_y[i][j] = header_height+i*panel_height+panel_height/2+(i+2)*offset_y;
////rectMode(CENTER);
rect(panel_center_x[i][j], panel_center_y[i][j], panel_width, panel_height);
}
}
//Write header text (Aircraft Code Sign, Type, Date/Time, App Name and Version
fill(255);
textFont(font, 18);
//Aircraft model
text("Aircraft Type: STEMVdrone", header_center_x-header_width/2.5, header_center_y);
//Call sign
text("Call Sign: STEM101", header_center_x-header_width/5, header_center_y);
//Date and Time
text("Date: "+ day() + "/" + month() + "/" + year() +" Time: "+ hour() + ":" + minute() + ":" +second(), header_center_x+header_width/2.75, header_center_y);
//Field Name
text("Field: Carson City Airfield", header_center_x, header_center_y);
}
void draw()
{
//Consider calling all static element draw methods in setup instead of draw
drawBackdrop();
MakeAnglesDependentOnMPU6050();
AirspeedIndicator(airspeed);
AttitudeIndicator(attitude,pitch);
Altimeter(altitude);
ACARS();
HeadingIndicator(heading);
//ShowAzimuth();
VerticalSpeedIndicator(vertical_speed);
/*
* START: Testing code and data
*/
if(TEST_MODE){
//Array of speeds to test the Airspeed Indicator
int[] airspeed_test = {40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,74,76,76,80};
AirspeedIndicator(airspeed_test[draw_counter]);
//Array of attitudes to test the Attitude Indicator
//int[] attitude_test = {5,5,4,4,3,3,2,2,1,1,0,0,-1,-1,-2,-2,-3,-3,-4,-4,-5,-5,-6,-6,-7,-7,-8,-8,-9,-9};
//float[] attitude_test = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};
float[] attitude_test = {-5,-5,-4,-3.5,-3,-2.5,-2,-1.5,-1,-1,0,1,1.5,1,1.5,2.5,2,2,2.5,3,3,3.5,3,3.5,3.5,3,2.5,2,1.5,1,0};
float[] pitch_test = {-5,-5,-4,-3.5,-3.0,-2.5,-2.0,-1.5,-1.0,0,1,2,3,4,5,6,7,6,6,6,5,5,4,4,3,3,3,2,1,0};
AttitudeIndicator(attitude_test[draw_counter], pitch_test[draw_counter]);
//Array of headings to test the Heading Indicator
int[] heading_test = {40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,74,76,76,80};
HeadingIndicator(heading_test[draw_counter]);
//Array of heights to test the Altimeter
int[] altimeter_test = {200,200,200,200,400,400,400,400,600,600,600,600,800,800,11200,11250,11350,11450,11550,11650,11750,11850,11950,12250,12500,12750,13000,13100,13250,13300,13310,13320,13330,13340,13350,13360,13370};
Altimeter(altimeter_test[draw_counter]);
//Array of Vertical Speeds to test the Vertical Speed Indicator
int[] verticalspeed_test = {1200,1250,1350,1450,1550,1650,1750,1850,1950,1250,1200,1275,1300,1310,1325,1330,1331,1332,1333,1334,1335,1336,1337,-200,-200,-200,-200,-400,-400,-400,-400,-600,-600,-600,-600,-800,-800};
VerticalSpeedIndicator(verticalspeed_test[draw_counter]);
//Increment to indicate number of draw loops, and count down when the input array length is reached
//Input array length capped at 30 values for now
if(direction == 1){
if(++draw_counter > 28) direction = -1;
} else {
if(--draw_counter < 2) direction = 1;
};
}
/*
* END: Testing code and data
*/
}
void serialEvent(Serial port) //Reading the datas by Processing.
{
//When reading match the way the ouput is printed in the Arduino sketch.
////Serial output is in reverse order, note when reading in Processing. Separator is whitespace and three values end in \n
//Print YPR as RPY
String input = port.readStringUntil('\n');
if(input != null){
input = trim(input);
//print(input);
String[] values = split(input, ",");
if(values.length==3){
ws = values[0];
x = float(values[1]);
y = float(values[2]);
print(x);
print(",");
println(y);
}
else if (values.length==4){
ws = values[0];
x = float(values[1]); //yaw
y = float(values[2]); //pitch
z = float(values[3]);//roll
print(x);
print(",");
print(y);
print(",");
println(z);
}
println (ws);
if (ws.equals("a") ){ //for MPU
Phi = z;
Theta = y;
Psi = x;
}
else if(ws.equals("b") ){ //for BMP
altitudem = x; //in meters
temperature = y;
altitude = (x*3.281); //in feet
}
else if (ws.equals("c") ){ //for GPS
latitude = x;
longitude = y;
airspeed = z;
}
}
}
void MakeAnglesDependentOnMPU6050()
{
attitude =-Phi/5;
pitch=Theta*10;
Azimuth=Psi;
pitch = Theta; //The p value of the ypr
attitude = Phi*10; //The p value of the ypr (amplify by 10 for better visualization
heading = Psi; //The p value of the ypr
}
/*
* AirspeedIndicator: Method to draw the Airspeed Indicator
* Sits in Panel Row 1, Col 1
*/
void AirspeedIndicator(float airspeed){
noStroke();
//Move origin to center of first panel
pushMatrix();
translate(panel_center_x[0][0], panel_center_y[0][0]);
//rectMode(CENTER);
//ellipseMode(CENTER);
//imageMode(CENTER);
/* START STATIC BACKGROUND ELEMENTS: Will not rotate */
// Angles for sin() and cos() start at 3 o'clock;
// subtract HALF_PI to make them start at the top
//Outer ring
fill(100); //light gray
ellipse(0, 0, gauge_dia, gauge_dia);
//Second ring which is the main dial
fill(50); //dark gray
ellipse(0, 0, gauge_dia*0.9, gauge_dia*0.9);
//Draw the tick marks
/*
rotate(PI/2+PI/3);
SpanAngle=300;
NumberOfScaleMajorDivisions=18;
NumberOfScaleMinorDivisions=36;
CircularScale(gauge_dia*0.7); //Upper circular scale
rotate(-PI/2-PI/3); //Reset upper scale marking rotation.
*/
CircularScale_new(gauge_dia*0.85, 30, 330, 19, 3); //Major scale
CircularScale_new(gauge_dia*0.85, 30, 330, 38, 2); //Minor scale
//Draw the range indicator arcs
//70-120: white, 80-140: green, 140-160: yellow, 160-220: red
noFill();
strokeWeight(5);
stroke(255); //white
arc(0, 0, gauge_dia*0.75, gauge_dia*0.75, -PI/6, HALF_PI+PI/6);
stroke(18,135,85); //green
arc(0, 0, gauge_dia*0.85, gauge_dia*0.85, HALF_PI, PI-PI/6);
stroke(255,242,0); //yellow
arc(0, 0, gauge_dia*0.85, gauge_dia*0.85, PI-PI/6, PI);
stroke(239,41,61); //red
//arc(0, 0, gauge_dia*0.85, gauge_dia*0.85, PI, PI+PI/3);
arc(0, 0, gauge_dia*0.85, gauge_dia*0.85, radians(180), radians(240));
//Draw the numbers from 40 to 220
/*(
fill(255); //text needs fill
textSize(15);
float count = -3.5;
float angle;
for(int i=min_scale_value;i<=max_scale_value;i=i+20){
angle = count*SpanAngle/NumberOfScaleMajorDivisions;
//text(""+i, gauge_dia*0.6/2*cos(radians(angle)),gauge_dia*0.6/2*sin(radians(angle)));
count+=2;
}
*/
//Draw the numbers from 40 to 220
NumericLabels(gauge_dia*0.55, 30, 330, 40, 220, 20);
//Draw the units name
text("MPH", 0,gauge_dia*0.3/2);
/* END STATIC BACKGROUND ELEMENTS: Will not rotate */
/* START MOVING ELEMENTS: Will rotate */
//Draw the needle
//Limit the input from 40 to 220
if(airspeed < 40) airspeed = 40;
if(airspeed > 220) airspeed = 220;
float scaled_airspeed = map(airspeed, 40, 220, 30, 330); //Mapping the airspeed range to the degrees the values span on the gauge
rotate(radians(scaled_airspeed));
noStroke();
fill(255);
triangle(-5, 0, 5, 0, 0, -gauge_dia*0.8/2);
/* END MOVING ELEMENTS: Will rotate */
rotate(-radians(scaled_airspeed));
popMatrix();
}
/*
* AttitudeIndicator: Method to draw the Attitude Indicator
* Sits in Panel Row 1, Col 2
*/
void AttitudeIndicator(float attitude, float pitch)
{
noStroke();
//Move origin to center of second panel
//For each panel keep the origin at the center of the panel while drawing within the panel. Push and Pop tranlations as required within the panel.
//Finally pop the translation to the center of the panel back the the default so subsequent panel drawing translations are from the default origin.
pushMatrix();
translate(panel_center_x[0][1], panel_center_y[0][1]);
////rectMode(CENTER);
////imageMode(CENTER);
////ellipseMode(CENTER);
fill(0, 180, 255); //sky blue
//sky blue rectangle, entire background
//rect(0, 0, panel_width*0.8, panel_height);
//Draw a circle for the sky and a semicircle for the earth to create a circular gauge
ellipse(0, 0, panel_width*0.625, panel_width*0.625);
fill(95, 55, 40); //earth brown
//rotates the entire image, so indicator needs to be counter-rotated so only background appears to rotate
//Move origin down a quarter panel height and draw earth recatangle to fill bottom half of panel (need since we are in //rectMode(CENTER)
//pushMatrix();
//translate(0,panel_height/4);
//earth brown semi-circle, lower half. Pitch reduces or increases the height of the semi-circle to simulate, well, Pitch (nose up/down)
rotate(-radians(attitude));
//rect(0, 0, panel_width*0.8, panel_height/2+pitch);
arc(0, 0, panel_width*0.625, panel_width*0.625, 0-radians(pitch), PI+radians(pitch), OPEN);
rotate(radians(attitude)); //Counter rotate so the markings and indicator are normal and only background appears rotated, simulating the bank
//popMatrix();
//translate(0,-panel_height/4); // move origin back to center of panel
//Parts on the AttitudeIndicator
PitchScale();
Axis();
Plane();
/* START STATIC BACKGROUND ELEMENTS: Will not rotate */
//rotate(-PI-PI/6);
//SpanAngle=120;
//NumberOfScaleMajorDivisions=12;
//NumberOfScaleMinorDivisions=24;
//CircularScale(gauge_dia*0.85); //Upper circular scale
//rotate(PI+PI/6); //Reset upper scale marking rotation.
//rotate(-PI/6); //This is to draw the lower scale markings at a rotated point.
//CircularScale(gauge_dia*0.85); //Lower circular scale
CircularScale_new(gauge_dia, 300, 60, 40, 2); //Upper circular scale
CircularScale_new(gauge_dia, 120, 240, 40, 2); //Lower circular scale
//rotate(PI/6); //Reset lower scale marking rotation.
/* END STATIC BACKGROUND ELEMENTS: Will not rotate */
popMatrix();
}
void ShowAzimuth()
{
fill(50);
noStroke();
rect(20, 470, 440, 50);
int Azimuth1=round(Azimuth);
textAlign(CORNER);
textSize(35);
fill(255);
text("Azimuth: "+Azimuth1+" Deg", 80, 477, 500, 60);
textSize(40);
fill(25,25,150);
text("FLIGHT SIMULATOR", -350, 477, 500, 60);
}
void Plane()
{
fill(0);
strokeWeight(1);
stroke(0, 255, 0);
triangle(-10, 0, 10, 0, 0, 10);
rect(45, 0, 60, 10);
rect(-45, 0, 60, 10);
}
void Axis()
{
//One vertical and one horiontal line red in color
stroke(255, 0, 0);
strokeWeight(3);
line(-30, 0, 30, 0);
line(0, gauge_dia/2*0.85, 0, -gauge_dia/2*0.85);
fill(100, 255, 100);
stroke(0);
//triangle(0, -285, -10, -255, 10, -255);
triangle(0, -gauge_dia/2*0.85, -10, -gauge_dia/2*0.85+10, 10, -gauge_dia/2*0.85+10);
//triangle(0, 285, -10, 255, 10, 255);
triangle(0, gauge_dia/2*0.85, -10, gauge_dia/2*0.85-10, 10, gauge_dia/2*0.85-10);
}
void PitchScale()
{
stroke(255);
fill(255);
strokeWeight(3);
textSize(12);
textAlign(CENTER);
for (int i=-4;i<5;i++)
{
if ((i==0)==false)
{
line(30, 20*i, -30, 20*i);
text(""+i*10, 50, 20*i+5, 100, 30);
text(""+i*10, -50, 20*i+5, 100, 30);
}
}
textAlign(CORNER);
strokeWeight(2);
for (int i=-9;i<10;i++)
{
if ((i==0)==false)
{
line(10, 10*i, -10, 10*i);
}
}
}
/*
* Altimeter: Method to draw the Altimeter
* Sits in Panel Row 1, Col 3
*/
void Altimeter(float altitude){
noStroke();
//Move origin to center of third panel
pushMatrix();
translate(panel_center_x[0][2], panel_center_y[0][2]);
//rectMode(CENTER);
//ellipseMode(CENTER);
//imageMode(CENTER);
/* START STATIC BACKGROUND ELEMENTS: Will not rotate */
// Angles for sin() and cos() start at 3 o'clock;
// subtract HALF_PI to make them start at the top
//Outer ring
fill(100); //light gray
ellipse(0, 0, gauge_dia, gauge_dia);
//Second ring which is the main dial
fill(50); //dark gray
ellipse(0, 0, gauge_dia*0.9, gauge_dia*0.9);
//Draw the tick marks
//rotate(-PI/2);
SpanAngle=360;
NumberOfScaleMajorDivisions=10;
NumberOfScaleMinorDivisions=50;
//CircularScale(gauge_dia*0.7); //Upper circular scale
//CircularScale_new(gauge_dia*0.85, 0, 360, 11, 3); //Major scale
//CircularScale_new(gauge_dia*0.85, 0, 360, 52, 2); //Minor scale
CircularScale_new(gauge_dia*0.85, 0, 360, 10, 3); //Major scale
CircularScale_new(gauge_dia*0.85, 0, 360, 50, 2); //Minor scale
//rotate(PI/2); //Reset upper scale marking rotation.
//Draw the numbers from 0 to 9
NumericLabels(gauge_dia*0.6, 0, 324, 0, 9, 1);
//fill(255); //text needs fill
//textSize(15);
//float start_angle=-90;
//float angle;
//for(int i=min_scale_value;i<=max_scale_value;i++){
//angle = start_angle + (SpanAngle/NumberOfScaleMajorDivisions)*i;
//text(""+i, gauge_dia*0.7/2*cos(radians(angle)),gauge_dia*0.6/2*sin(radians(angle)));
//}
//Draw the units name
text("FEET", 0,gauge_dia*0.3/2);
/* END STATIC BACKGROUND ELEMENTS: Will not rotate */
/* START MOVING ELEMENTS: Will rotate */
//Each needle will have to be rotated separately
//First extract the 10,000 feet, 1000 feet and 100 feet multipliers
int tenthoufeet = (int) altitude/10000;
int thoufeet = (int) (altitude-(tenthoufeet*10000))/1000; //subtract the 10,000 feet value to get the thousands
int hundfeet = (int) (altitude-(tenthoufeet*10000)-(thoufeet*1000))/100; //subtract the 10,000 and 1000feet values to get the hundreds
int tenfeet = (int) (altitude-(tenthoufeet*10000)-(thoufeet*1000)-(hundfeet*100)); //subtract the 10,000 and 1000feet values to get the hundreds
//And then map each value to the scale
float scaled_tenthoufeet = map(tenthoufeet, 0, 10, 0, 360); //Mapping the airspeed range to the degrees the values span on the gauge
float scaled_thoufeet = map(thoufeet, 0, 10, 0, 360); //Mapping the airspeed range to the degrees the values span on the gauge
float scaled_hundfeet = map(hundfeet, 0, 10, 0, 360); //Mapping the airspeed range to the degrees the values span on the gauge
//Display the tens of feet as text
noStroke();
fill(100);
rect(gauge_dia*0.6/2, 0, 20, 15);
fill(255); //text needs fill
textSize(12);
textAlign(CENTER, TOP);
text(""+tenfeet, gauge_dia*0.6/2, -6);
//Draw the needles, different shapes and individual rotation
//10,000 feet needle - short shape
rotate(radians(scaled_tenthoufeet));
noStroke();
fill(255);
PShape tenthoufeet_needle; // The PShape object
tenthoufeet_needle = createShape();
tenthoufeet_needle.beginShape();
tenthoufeet_needle.fill(255,165,0);
tenthoufeet_needle.noStroke();
tenthoufeet_needle.vertex(-5, 0);
tenthoufeet_needle.vertex(5, 0);
tenthoufeet_needle.vertex(8, -gauge_dia*0.8/4);
tenthoufeet_needle.vertex(0, -gauge_dia*0.8/3);
tenthoufeet_needle.vertex(-8, -gauge_dia*0.8/4);
tenthoufeet_needle.vertex(-5, 0);
tenthoufeet_needle.endShape(CLOSE);
shape(tenthoufeet_needle, 0, 0);
rotate(-radians(scaled_tenthoufeet));
//1000 feet needle, long rectangle with triange tip
rotate(radians(scaled_thoufeet));
noStroke();
fill(255);
triangle(-5, 0, 5, 0, 0, -gauge_dia*0.8/2);
rotate(-radians(scaled_thoufeet));
//100 feet needle, longest, a thin rectangle with an inverted triangle at the tip
rotate(radians(scaled_hundfeet));
stroke(255);
strokeWeight(2);
line(0, 0, 0, -gauge_dia/2*0.75);
noStroke();
fill(255);
triangle(0, -gauge_dia/2*0.75, -10, -(gauge_dia/2*0.75)-10, 10, -(gauge_dia/2*0.75)-10); //inverted triangle at tip
rotate(-radians(scaled_hundfeet));
/* END MOVING ELEMENTS: Will rotate */
popMatrix();
}
/*
* ACARS: Method to draw the ACARS Panel
* Sits in Panel Row 2, Col 1
*/
void ACARS(){
//Move origin to center of panel
pushMatrix();
translate(panel_center_x[1][0], panel_center_y[1][0]);
rectMode(CORNER); //For ACARS only, reset at the end
//Heading
fill(255); //white
textFont(font, 18);
text("ACARS", 0, -0.9*panel_height/2);
float left_col_offset = -0.9*panel_width/2; //minus
float right_col_offset = 0.9*panel_width/2; //plus
float leading_space = 20;
float line_offset = -0.7*panel_height/2;
//Left align left column
textAlign(LEFT);
//Line 1
fill(255);
textFont(font, 14);
//First line so no leading space
text("latitude", left_col_offset, line_offset);
//Line 2
fill(54, 161, 255); //blue
textFont(font, 18);
line_offset += leading_space;
text(latitude, left_col_offset, line_offset); //FLT NO
//Line 3
fill(255);
textFont(font, 14);
line_offset += leading_space;
text("longitude", left_col_offset, line_offset);
//Line 4
fill(54, 161, 255); //blue
textFont(font, 18);
line_offset += leading_space;
text(longitude, left_col_offset, line_offset); //DATE
//Line 5
fill(255);
textFont(font, 14);
line_offset += leading_space;
text("speed", left_col_offset, line_offset);
//Line 6
fill(54, 161, 255); //blue
textFont(font, 18);
line_offset += leading_space;
text(airspeed, left_col_offset, line_offset); //START
//Line 5
fill(255);
textFont(font, 14);
line_offset += leading_space;
text("altitude(metres)", left_col_offset, line_offset);
//Line 6
fill(54, 161, 255); //blue
textFont(font, 18);
line_offset += leading_space;
text(altitudem, left_col_offset, line_offset); //END
//Line 7
//fill(255);
//textFont(font, 14);
//line_offset += leading_space;
//text("NOTAM", left_col_offset, line_offset);
////Line 8
//fill(255,0,0); //blue
//textFont(font, 18);
//line_offset += leading_space;
//text("All flights to land immediately.", left_col_offset, line_offset); //END
//Right align right column
textAlign(RIGHT);
line_offset = -0.7*panel_height/2; //reset line_offset for right column
//Line 1
fill(255);
textFont(font, 14);
//First line so no leading space
text("temperature", right_col_offset, line_offset);
//Line 2
fill(26, 182, 99); //green
textFont(font, 18);
line_offset += leading_space;
text(temperature, right_col_offset, line_offset); //red if there is a conflict with any other flyer
////Line 3
//fill(255);
//textFont(font, 14);
//line_offset += leading_space;
//text("FUEL QTY", right_col_offset, line_offset);
////Line 4
//fill(54, 161, 255); //blue
//textFont(font, 18);
//line_offset += leading_space;
//text("[ ] cc", right_col_offset, line_offset);
////Line 5
//fill(255);
//textFont(font, 14);
//line_offset += leading_space;
//text("WEIGHT", right_col_offset, line_offset);
////Line 6
//fill(54, 161, 255); //blue
//textFont(font, 18);
//line_offset += leading_space;
//text("[ ] lbs", right_col_offset, line_offset);
////Line 7
//fill(255);
//textFont(font, 14);
//line_offset += leading_space;
//text("WIND DIR", right_col_offset, line_offset);
////Line 8
//fill(54, 161, 255); //blue
//textFont(font, 18);
//line_offset += leading_space;
//text("NE Mild", right_col_offset, line_offset);
//reset all modes, rotations and translations
rectMode(CENTER);
popMatrix();
}
/*
* HeadingIndicator: Method to draw the Heading Indicator
* Sits in Panel Row 2, Col 2
*/
void HeadingIndicator(float heading)
{
noStroke();
pushMatrix();
//Move origin to center of panel
translate(panel_center_x[1][1], panel_center_y[1][1]);
//rectMode(CENTER);
//ellipseMode(CENTER);
//imageMode(CENTER);
/*
* START HEADING ELEMENTS: All elements below will rotate to indicate heading
*/
rotate(radians(heading));
//Outer ring, with direction letters
fill(100); //light gray
ellipse(0, 0, gauge_dia, gauge_dia);
//Second from outer ring, with tick marks
fill(75); //dark gray
ellipse(0, 0, gauge_dia*0.8, gauge_dia*0.8);
strokeWeight(20);
NumberOfScaleMajorDivisions=18;
NumberOfScaleMinorDivisions=36;
SpanAngle=180;
//CircularScale(gauge_dia*0.625);
CircularScale_new(gauge_dia*0.775, 0, 360, 8, 3); //scale_dia = 236*0.775 = 183
CircularScale_new(gauge_dia*0.775, 0, 360, 40, 2); //scale_dia = 236*0.775 = 183
rotate(PI);
SpanAngle=180;
//CircularScale(gauge_dia*0.625);
rotate(-PI);
fill(255);
textSize(20);
textAlign(CENTER);
text("W", -gauge_dia/2*0.9, 0);
text("E", gauge_dia/2*0.9, 0);
text("N", 0, -gauge_dia/2*0.85);
text("S", 0, gauge_dia/2*0.95);
rotate(PI/4);
textSize(15);
text("NW", -gauge_dia/2*0.9, 0);
text("SE", gauge_dia/2*0.9, 0);
text("NE", 0, -gauge_dia/2*0.85);
text("SW", 0, gauge_dia/2*0.95);
rotate(-PI/4);
//Draw an orange line from center to North
stroke(255,165,0);
line(0, 0, 0, -gauge_dia/2*0.775);
//Inner circle
noStroke();
fill(0, 180, 255); //sky blue
ellipse(0, 0, gauge_dia*0.6, gauge_dia*0.6);
//Draw the wind direction needle
WindDirection(45);
rotate(-radians(heading));
/*
* END HEADING ELEMENTS: All elements above will rotate to indicate heading
*/
/* START STATIC BACKGROUND ELEMENTS: Will not rotate */
PImage img;
img = loadImage("airplane_icon_orange.png");
image(img, 0, 0, gauge_dia*0.4, gauge_dia*0.4);
/* END STATIC BACKGROUND ELEMENTS: Will not rotate */
popMatrix(); //Reset origin to default
}
/*
* WindDirection: Method to draw the wind direction needle inside the Heading Indicator
* Sits in Panel Row 2, Col 2 Called inside the Heading Indicator so already translated, not required again
*/
void WindDirection(float wind_direction)
{
/*
* The wind direction indicator will also rotate along with the background
*/
rotate(radians(wind_direction));
//Draw the wind direction needle
stroke(0, 0, 255);
strokeWeight(3);
line(0, gauge_dia/2*0.55, 0, -gauge_dia/2*0.5);
fill(0, 0, 255);
noStroke();
triangle(0, -gauge_dia/2*0.55, -10, -gauge_dia/2*0.55+10, 10, -gauge_dia/2*0.55+10);
rotate(-radians(wind_direction));
/*
* END Wind Direction
*/
}
/*
* VerticalSpeedIndicator: Method to draw the Vertical Speed Indicator
* Sits in Panel Row 2, Col 3
*/
void VerticalSpeedIndicator(float vertical_speed){
noStroke();
//Move origin to center of panel row 2, col 3
pushMatrix();
translate(panel_center_x[1][2], panel_center_y[1][2]);
/* START STATIC BACKGROUND ELEMENTS: Will not rotate */
// Angles for sin() and cos() start at 3 o'clock;
// subtract HALF_PI to make them start at the top
//Outer ring
fill(100); //light gray
ellipse(0, 0, gauge_dia, gauge_dia);
//Second ring which is the main dial
fill(50); //dark gray
ellipse(0, 0, gauge_dia*0.9, gauge_dia*0.9);
//Draw the tick marks for the Climb (upper) scale
/*
rotate(-PI/2);
SpanAngle=180;
NumberOfScaleMajorDivisions=6;
NumberOfScaleMinorDivisions=30;
CircularScale(gauge_dia*0.7); //Upper circular scale
rotate(PI/2); //Reset upper scale marking rotation.
*/
CircularScale_new(gauge_dia*0.85, 270, 90, 6, 3); //Upper scale
CircularScale_new(gauge_dia*0.85, 90, 270, 6, 3); //Lower scale
//Draw minor ticks between 0 and 1
CircularScale_new(gauge_dia*0.85, 240, 300, 21, 2); //Upper scale
//Draw the numbers from 1 to 6
//The gauge diameters for upper and lowerneed to be slightly different. Possibly because they run different arcs lengths
NumericLabels(gauge_dia*0.6, 270, 90, 0, 6, 1); //Upper scale
NumericLabels(gauge_dia*0.6, 90, 270, 6, 0, -1); //Lower scale
//Draw the tick marks for the Descend (lower) scale
rotate(PI/2);
SpanAngle=180;
NumberOfScaleMajorDivisions=6;
NumberOfScaleMinorDivisions=30;
//CircularScale(gauge_dia*0.7); //Upper circular scale
rotate(-PI/2); //Reset upper scale marking rotation.
//Draw the numbers from 0 to 6 for the Climb (upper) scale
/*
fill(255); //text needs fill
textSize(15);
float start_angle=-180;
float angle;
for(int i=min_scale_value;i<=max_scale_value;i++){
angle = start_angle + (SpanAngle/NumberOfScaleMajorDivisions)*i;
text(""+i, gauge_dia*0.6/2*cos(radians(angle)),gauge_dia*0.6/2*sin(radians(angle)));
}
//Draw the numbers from 1 to 5 for the Descend (lower) scale (0 and 6 have already been drawn)
start_angle=-180;
for(int i=min_scale_value+1;i<=max_scale_value-1;i++){
angle = start_angle - (SpanAngle/NumberOfScaleMajorDivisions)*i;
text(""+i, gauge_dia*0.6/2*cos(radians(angle)),gauge_dia*0.6/2*sin(radians(angle)));
}
*/
//Draw the units name
text("x100", 0,gauge_dia*0.1/2);
text("FEET/MIN", 0,gauge_dia*0.3/2);
//Draw the UP/DN text
textSize(12);
text("UP", -gauge_dia*0.75/2*cos(radians(45)),-gauge_dia*0.75/2*sin(radians(45)));
text("DN", -gauge_dia*0.75/2*cos(radians(45)),gauge_dia*0.75/2*sin(radians(45)));
/* END STATIC BACKGROUND ELEMENTS: Will not rotate */
/* START MOVING ELEMENTS: Will rotate */
//And then map each value to the scale. Since this is a dual scale for +ve and -ve values, conditional mapping required
float scaled_vertical_speed=0;
if(vertical_speed >= 0){
if(vertical_speed > 6000) vertical_speed = 6000; //limit to +6000 feet/minute (climb)
scaled_vertical_speed = map(vertical_speed, 0, 6000, -90, 90); //Mapping the airspeed range to the degrees the values span on the gauge
}
if(vertical_speed < 0){
if(vertical_speed < -6000) vertical_speed = -6000; //limit to -6000 feet/minute (descent)
scaled_vertical_speed = map(vertical_speed, -6000, 0, 90, 270); //Mapping the airspeed range to the degrees the values span on the gauge
}
//Needle, long rectangle with triange tip
rotate(radians(scaled_vertical_speed));
noStroke();
fill(255);
triangle(-5, 0, 5, 0, 0, -gauge_dia*0.8/2);
rotate(-radians(scaled_vertical_speed));
/* END MOVING ELEMENTS: Will rotate */
popMatrix();
}
/*
* Circular Scale: Generic method to draw circular tick marks. This will have to be called for major and minor ticks marks separately. A third length is also possible.
* Accepts the following parameters (all float types for accuracy rounding skews the drawing):
* scale_dia: The diameter of the virtual circle along which the tick marks will be drawn, inward from the circumference.
* start_angle: The angle in degrees from which the ticks marks will be drawn (any value between 0 and 360). 0 is by default 3 o'clock but will be rotated to be 12 o'clock
* end_angle: The angle in degrees to which the ticks marks will be drawn (any value between 0 and 360).
* num_ticks: The number of tick marks to be drawn, including first and last. First one will be at start angle and the last one will be at the end_angle. Min 2, Max 360.
* tick_size: 1=short, 2=medium, 3=large (always draw tick marks from small to large)
*
* Notes: IF any parameters are incorrect the following defaults will be assumed:
* start_angle = 0, end_angle = 359, num_ticks = 360, tick_size = 2
*/
void CircularScale_new(float scale_dia, float start_angle, float end_angle, float num_ticks, int tick_size)
{
float StrokeWidth=1;
strokeWeight(StrokeWidth);
stroke(255);
//test line
//line(0,0,scale_dia,scale_dia);
float gap_angle=0, current_angle=0;
float inner_x, inner_y, outer_x, outer_y; //Draw the tick mark as a line between these two points calculated using the scale diameter and angle
float outer_inner_diff=0; //this is the tick length
//Check for valid inputs or set defaults
if(start_angle < 0) start_angle = 0;
if(start_angle > 360) start_angle = 360;
if(end_angle < 0) start_angle = 0;
if(end_angle > 360) start_angle = 360;
if(num_ticks < 2) num_ticks = 2;
if(num_ticks > 360) num_ticks = 360;
if(tick_size != 1 && tick_size != 2 && tick_size != 3) tick_size = 2;
//Set a outer_inner_diff as a percentage of the scale diameter (depending on the tick_size parameter 1=short, 2=medium and 3=long values)
//The tick length is the difference between inner and outer length
if(tick_size == 1){ //short
outer_inner_diff = scale_dia/30;
}
else if(tick_size == 2){ //medium
outer_inner_diff = scale_dia/20;
}
if(tick_size == 3){ //long
outer_inner_diff = scale_dia/10;
}
if(end_angle > start_angle){ //for example 90 to 180 (6 to 9 o'clock on a clock face)
gap_angle = (end_angle - start_angle)/num_ticks; //first and last tick will be at the start and end angles
}
else{ //for example 180 to 90 (9 to 6 o'clock on a clock face)
//gap_angle = 360 - (start_angle - end_angle)/(num_ticks-1);
gap_angle = (360 - (start_angle - end_angle))/num_ticks;
}
//0 is by default 3 o'clock so rotate such that it is 12 o'clock
rotate(-PI/2);
for (float tick_count=0;tick_count<num_ticks;tick_count++)
{
current_angle = start_angle + gap_angle*tick_count;
//If current_angle > 360 subtract 360 ( the angle crosses the 0 degree mark, usually when end_angle less than start_angle)
if(current_angle >360) current_angle -= 360;
inner_x = (scale_dia/2 - outer_inner_diff) * cos(radians(current_angle));
inner_y = (scale_dia/2 - outer_inner_diff) * sin(radians(current_angle));
outer_x = scale_dia/2 * cos(radians(current_angle));
outer_y = scale_dia/2 * sin(radians(current_angle));
//Draw tick mark
line(inner_x, inner_y, outer_x, outer_y);
}
//Cancel rotation
rotate(PI/2);
}
void NumericLabels(float scale_dia, int start_angle, int end_angle, int min_value, int max_value, int increment)
{
fill(255);
textSize(15);
float gap_angle=0, current_angle=0;
float x, y; //label coordinates
int [] labels = new int[360]; //Max labels unlikely to exceed 360. Check for dynamic array options
int label_count=0;
//Determine label values and count based on min, max and increment values
if(increment > 0){ //positive increment, max greater than min
for(int i=min_value; i<=max_value; i=i+increment){
labels[label_count] = i;
label_count++;
}
}
if(increment < 0){ //negative increment, max less than min
for(int i=min_value; i>=max_value; i=i+increment){
labels[label_count] = i;
label_count++;
}
}
//Check for valid inputs or set defaults
if(start_angle < 0) start_angle = 0;
if(start_angle > 360) start_angle = 360;
if(end_angle < 0) start_angle = 0;
if(end_angle > 360) start_angle = 360;
if(end_angle > start_angle){ //for example 90 to 180 (6 to 9 o'clock on a clock face)
gap_angle = (end_angle - start_angle)/(label_count-1);
}
else{ //for example 180 to 90 (9 to 6 o'clock on a clock face)
gap_angle = (360 - (start_angle - end_angle))/(label_count-1);
}
//0 is by default 3 o'clock so rotate such that it is 12 o'clock
//However, rotate does not work as it rotates the text as well. So offset the angle values by -PI/2
for (int i=0; i<label_count; i++)
{
current_angle = start_angle + gap_angle*i; //in degrees
//If current_angle > 360 subtract 360 ( the angle crosses the 0 degree mark, usually when end_angle less than start_angle)
if(current_angle >360) current_angle -= 360;
x = scale_dia/2 * cos(radians(current_angle) - PI/2);
y = scale_dia/2 * sin(radians(current_angle) - PI/2);
//Draw label
textAlign(CENTER, CENTER);
text(""+labels[i], x, y);
}
}
void CircularScale(float scale_dia)
{
float StrokeWidth=1;
float an;
float DivxPhasorCloser;
float DivxPhasorDistal;
float DivyPhasorCloser;
float DivyPhasorDistal;
strokeWeight(StrokeWidth);
stroke(255);
float DivCloserPhasorLength=scale_dia*0.7-scale_dia/9-StrokeWidth; //This is an arbitrary calculation to get the length of the radial line
float DivDistalPhasorLength=scale_dia*0.7-scale_dia/6.5-StrokeWidth; //The dividends (9 and 7.5) to get a difference bwteen the two radial lines whick is the tick mark
for (int Division=0;Division<NumberOfScaleMinorDivisions+1;Division++)
{
an=SpanAngle/2+Division*SpanAngle/NumberOfScaleMinorDivisions;
DivxPhasorCloser=DivCloserPhasorLength*cos(radians(an));
DivxPhasorDistal=DivDistalPhasorLength*cos(radians(an));
DivyPhasorCloser=DivCloserPhasorLength*sin(radians(an));
DivyPhasorDistal=DivDistalPhasorLength*sin(radians(an));
line(DivxPhasorCloser, DivyPhasorCloser, DivxPhasorDistal, DivyPhasorDistal);
}
DivCloserPhasorLength=scale_dia*0.7-scale_dia/10-StrokeWidth;
DivDistalPhasorLength=scale_dia*0.7-scale_dia/6.4-StrokeWidth;
for (int Division=0;Division<NumberOfScaleMajorDivisions+1;Division++)
{
an=SpanAngle/2+Division*SpanAngle/NumberOfScaleMajorDivisions;
DivxPhasorCloser=DivCloserPhasorLength*cos(radians(an));
DivxPhasorDistal=DivDistalPhasorLength*cos(radians(an));
DivyPhasorCloser=DivCloserPhasorLength*sin(radians(an));
DivyPhasorDistal=DivDistalPhasorLength*sin(radians(an));
if (Division==NumberOfScaleMajorDivisions/2|Division==0|Division==NumberOfScaleMajorDivisions)
{
strokeWeight(8);
stroke(0);
line(DivxPhasorCloser, DivyPhasorCloser, DivxPhasorDistal, DivyPhasorDistal);
strokeWeight(4);
stroke(100, 255, 100);
line(DivxPhasorCloser, DivyPhasorCloser, DivxPhasorDistal, DivyPhasorDistal);
}
else
{
strokeWeight(1);
stroke(255);
line(DivxPhasorCloser, DivyPhasorCloser, DivxPhasorDistal, DivyPhasorDistal);
}
}
}