element14 Community
element14 Community
    Register Log In
  • Site
  • Search
  • Log In Register
  • Community Hub
    Community Hub
    • What's New on element14
    • Feedback and Support
    • Benefits of Membership
    • Personal Blogs
    • Members Area
    • Achievement Levels
  • Learn
    Learn
    • Ask an Expert
    • eBooks
    • element14 presents
    • Learning Center
    • Tech Spotlight
    • STEM Academy
    • Webinars, Training and Events
    • Learning Groups
  • Technologies
    Technologies
    • 3D Printing
    • FPGA
    • Industrial Automation
    • Internet of Things
    • Power & Energy
    • Sensors
    • Technology Groups
  • Challenges & Projects
    Challenges & Projects
    • Design Challenges
    • element14 presents Projects
    • Project14
    • Arduino Projects
    • Raspberry Pi Projects
    • Project Groups
  • Products
    Products
    • Arduino
    • Avnet Boards Community
    • Dev Tools
    • Manufacturers
    • Multicomp Pro
    • Product Groups
    • Raspberry Pi
    • RoadTests & Reviews
  • Store
    Store
    • Visit Your Store
    • Choose another store...
      • Europe
      •  Austria (German)
      •  Belgium (Dutch, French)
      •  Bulgaria (Bulgarian)
      •  Czech Republic (Czech)
      •  Denmark (Danish)
      •  Estonia (Estonian)
      •  Finland (Finnish)
      •  France (French)
      •  Germany (German)
      •  Hungary (Hungarian)
      •  Ireland
      •  Israel
      •  Italy (Italian)
      •  Latvia (Latvian)
      •  
      •  Lithuania (Lithuanian)
      •  Netherlands (Dutch)
      •  Norway (Norwegian)
      •  Poland (Polish)
      •  Portugal (Portuguese)
      •  Romania (Romanian)
      •  Russia (Russian)
      •  Slovakia (Slovak)
      •  Slovenia (Slovenian)
      •  Spain (Spanish)
      •  Sweden (Swedish)
      •  Switzerland(German, French)
      •  Turkey (Turkish)
      •  United Kingdom
      • Asia Pacific
      •  Australia
      •  China
      •  Hong Kong
      •  India
      •  Korea (Korean)
      •  Malaysia
      •  New Zealand
      •  Philippines
      •  Singapore
      •  Taiwan
      •  Thailand (Thai)
      • Americas
      •  Brazil (Portuguese)
      •  Canada
      •  Mexico (Spanish)
      •  United States
      Can't find the country/region you're looking for? Visit our export site or find a local distributor.
  • Translate
  • Profile
  • Settings
Just Encase
  • Challenges & Projects
  • Design Challenges
  • Just Encase
  • More
  • Cancel
Just Encase
Blog Orb-Weaver Rover - Blog #6 - Project Overview
  • Blog
  • Forum
  • Documents
  • Events
  • Polls
  • Files
  • Members
  • Mentions
  • Sub-Groups
  • Tags
  • More
  • Cancel
  • New
Join Just Encase to participate - click to join for free!
  • Share
  • More
  • Cancel
Group Actions
  • Group RSS
  • More
  • Cancel
Engagement
  • Author Author: milosrasic98
  • Date Created: 28 Feb 2022 10:38 PM Date Created
  • Views 3388 views
  • Likes 13 likes
  • Comments 3 comments
Related
Recommended

Orb-Weaver Rover - Blog #6 - Project Overview

milosrasic98
milosrasic98
28 Feb 2022
Orb-Weaver Rover - Blog #6 - Project Overview

Orbweaver Rover

Orb-weaver Rover - Making the Rover! <----- Previous Blog

Table of Contents

  • Orbweaver Rover
  • 1. Introduction
  • 2. Rover additions
    • Box pick-up system
    • Rover Sensors
  • 3. Through the blogs
    • Blog #1 - Orbweaver Rover - Concept
    • Blog #2 - Monitoring and data collection (GCS, Sensor Boxes, GUI)
    • Blog #3 - Full Battery Test
    • Blog #4 - Making the Rover!
    • Blog #5 - Rover Electronics & First Moves!!!
  • 4. Final Software
    • Sensor Box Code
    • GCS Code
    • Rover Arduino Mega Code
    • Rover Arduino MKR WAN Code
    • GUI Code
  • 5. Testing the System
    • Freezer Test
    • Obstacle Test
    • Picking up the sensor box
  • 6. Summary
    • Real-life use cases 
    • What went well
    • What went wrong
    • Sketches
    • Closing Note

image1. Introduction

Hi! This will be my 6th and last blog for the Just Encase Design Challenge. In the previous 5 blogs, I've gone over my initial idea and calculations for the rover, designing a monitoring system and also doing some processing on the data, and of course, making the rover which is a sensor box on wheels. There have been a lot of things that went great with the project, but also a lot of setbacks as is normal with any project. In this blog, I will:

  1. Show the changes I made to the rover compared to the previous blog
  2. Go through all of the blogs and what I've accomplished in them
  3. Show off the final setup of the whole system
  4. Talk about what went great and what went wrong
  5. Go through where this kind of concept could actually be used

A lot of stuff to cover, let's begin with the changes on the rover compared to the 5th blog.

2. Rover additions

Compared to the previous blog, there are 2 major changes to the rover (and the whole system) when it comes to hardware. I finally installed the things needed for the rover to actually pick up and leave the sensor boxes and I've added 2 sensors to the rover. Let's begin with the box pick-up system.

Box pick-up system

To pick up the boxes I decided to not add any additional electronics since I will be only picking up a single sensor box for the tests. To do this, I wanted to use the continuous servo that controls the pitch of the rover body. My idea is to tilt the whole body by 90 degrees so I can get closer to the box to pick it up. A fork-like part will be mounted to the rover so it centers the box as it goes against it, while the sensor box will have a disk added on top of it. Here is what those parts look like.

imageimage

You can see a small spacer at the back of the fork. During the first tests I noticed that the fork didn't really reach the disk, so I printed an additional 10mm spacer so it can pick up the box a bit easier. The fork and spacer are mounted to the body of the rover with 2 M3 screws at the back. Here is how that looks.

image

I'll have a full demonstration of how this works at the end of this blog!

Rover Sensors

While the main function of the rover is to transport the sensor boxes around, casting a net of them, there isn't anything stopping us when it comes to mounting the sensors to the rover as well. It has the same Arduino as the sensor boxes, and it's mobile, which means we can go and measure whatever we want, wherever we want. I decided to add 2 sensors, MQ-7 and MQ-135. MQ sensors are great for detecting toxic gasses, but there's a reason they are not on the sensor boxes. The sensor boxes use AA batteries and are supposed to be left alone for a long time somewhere to monitor the parameters we are looking at. These sensors have a heater inside that draws around 150mA, which would drain the battery really fast.

imageimage

That would cover the major hardware changes compared to the last blog. Here is how they look in real life.

image

3. Through the blogs

In this section, I will go through what has been in each of the blogs before I get to the final point where we are at the moment. There are 6 blogs in total if we count this one or 5 blogs that came before this blog. I tried covering a single subject with each of the blogs so that they are useful on their own. Let's take a look at them.

  1. Blog #1 - Orbweaver Rover - Concept
  2. Blog #2 - Monitoring and data collection (GCS, Sensor Boxes, GUI)
  3. Blog #3 - Full Battery Test
  4. Blog #4 - Making the Rover!
  5. Blog #5 - Rover Electronics & First Moves!!!

Blog #1 - Orbweaver Rover - Concept

In the first blog, I covered some basic math and simulations behind the rover. This is how I ended up with the reduction ratio of 3. Those calculations and simulations helped me a lot to get a grasp of what kind of things I should be aiming for and what I can expect when it comes to the rover. Besides the simulations, I've done a bunch of calculations on what the angles of the wheels should be, as well as the speed of the wheels during different kinds of turns and so on.

imageimage

Blog #2 - Monitoring and data collection (GCS, Sensor Boxes, GUI)

In the second blog, I first did a breadboard test where I went out to see what kind of range I can expect with LoRa. I managed to do over 150m through a lot of houses between the 2 Arduinos. I then went on and designed a sensor box and a ground control station for this system. This is also where I took some of the first measurements and started working on the GUI that became what it is now.

imageimageimage

This is also the first place where the Hammond enclosures showed their strength. The box was left outside in -10C weather with light rain and didn't have a single problem!

Blog #3 - Full Battery Test

I dedicated this blog to some further tests of the system I've made in blog #2. I put a fresh set of batteries into the sensor box, and this time, instead of leaving it on my window, I put it on the ground in my yard. While the temperatures weren't as freezing as in the previous tests, they still dropped to about 0C. This time though, the box was subjected to rain and to the wet grass around, but again, no problems at all. The test ran for an impressive 45 hours, where I've managed to gather around 27000 measurements. The readings were at 5-second intervals, If I had put a bigger delay, the test would have ran for much longer.

imageimage

Blog #4 - Making the Rover!

Now we come to the fun (and hard) part, making the rover. This is something I've been doing in the background during the whole design challenge. But started making it once I finalized the design. The main construction of the rover was made using 3D printed parts (PLA), PVC pipes, and a Hammond Enclosure as a body. The enclosure is incredibly sturdy, with really thick walls, so I put together everything by threading holes into the sides and the bottom of the enclosure. A bit of Loctite can be used on the screws to make sure that the holes are waterproof. At the end of this blog, I had a full construction of the rover with active suspension. My rover suspension is inspired by the real rovers, that's why I went with a differential suspension. If you're interested in more details, you can check out the blog where I went into a lot of detail about the design.

imageimage

You don't have permission to edit metadata of this video.
Edit media
x
image
Upload Preview
image

You don't have permission to edit metadata of this video.
Edit media
x
image
Upload Preview
image

Blog #5 - Rover Electronics & First Moves!!!

Now we come to the blog that comes before this one, the electronics and first moves of the rover. In this blog, I began by wiring all of the motors and getting all of the wires through the pipes into the body of the rover. With all of the cables now in the body of the rover, I also had to tackle how to mount the electronics inside. I tried being as neat and organized as possible here because I knew that If I just pushed everything in, a wire will go loose somewhere and I'll lose a lot of time trying to find which one it is. I designed small 3D printed brackets for holding the electronics.

imageimage

I also stuck with using crimped-on nylon connectors, since they are easy to crimp and easy to disconnect if needed. I made sure to print labels for all of the cable ends so that there are no mistakes of what goes where. While it took a bit of time initially to do, it saved me so much time later, because I was constantly taking things out and putting them back in as I was working on the rover and troubleshooting problems.

imageimage

Everything fit inside just perfectly. I left a bit more slack than I needed to on the cables. While it looks a bit messy, it meant I didn't have any places where I had to go and replace a cable or solder on extensions to that cable. The lid on top just perfectly enclosed everything. After all of that, we come to the most rewarding part where we actually test out the rover!

You don't have permission to edit metadata of this video.
Edit media
x
image
Upload Preview
image

You don't have permission to edit metadata of this video.
Edit media
x
image
Upload Preview
image

With that trip through all of the blogs complete, it's time to see how the final setup performs. While there aren't many major hardware changes compared to the 5th blog, there are a lot of software changes.

4. Final Software

When it comes to software, for this last blog, I pretty much changed the codes for all of the components of the system, these include:

  1. Code for the Sensor Boxes
  2. GCS code
  3. Rover Arduino Mega Code
  4. Rover Arduino MKR WAN code
  5. GUI code

Here are the final versions of the codes mentioned above.

Sensor Box Code

This code is designed to read the data from the sensors that are onboard the sensor box and send them using LoRa to our GCS. To differentiate between the different boxes, they send their own unique box ID in the message. In my case, the Arduino read the data from a BMP280, a DHT22, the battery voltage, and the time from an RTC. It goes into low power mode to save power between readings. As mentioned above, it's good for about 27000 measurements using some no-brand batteries.

/*
 * ORBWEAVER ROVER PROJECT
 * Code for the Sensor Box module
 */

// Libraries
#include "ArduinoLowPower.h"
#include "DHT.h"
#include <Wire.h>

//Libraries
#include <Adafruit_BMP280.h>
#include <ds3231.h>
#include <SPI.h>
#include <LoRa.h>

// ID of this sensor box - unique for each box
#define BOX_ID 1

// MOSFET Pin
#define MOSFET 3

// DHT22
#define DHTPIN 2
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);
float temperature;
float humidity;
float heatIndex;

// Pressure sensor
Adafruit_BMP280 bmp; // use I2C interface
Adafruit_Sensor *bmp_temp = bmp.getTemperatureSensor();
Adafruit_Sensor *bmp_pressure = bmp.getPressureSensor();
double pressure = 0;

// RTC module
struct ts t;
String current_time;
String current_date;

// Rain sensor
int rain_sensor = 0;

// LoRa counter
int msg_counter = 0;

// Battery voltage
float battery_voltage = 3.0;
float voltage;

void setup() {

  // Starting serial
  Serial.begin(9600);

  // Setting up pins
  pinMode(MOSFET, OUTPUT);

  // Setting up the ADC
  analogReadResolution(10);

  // DHT22
  dht.begin();

  // Pressure Sensor
  unsigned status;
  status = bmp.begin(0x76, 0x58);

  /* Default settings from datasheet. */
  bmp.setSampling(Adafruit_BMP280::MODE_NORMAL,     /* Operating Mode. */
                  Adafruit_BMP280::SAMPLING_X2,     /* Temp. oversampling */
                  Adafruit_BMP280::SAMPLING_X16,    /* Pressure oversampling */
                  Adafruit_BMP280::FILTER_X16,      /* Filtering. */
                  Adafruit_BMP280::STANDBY_MS_500); /* Standby time. */

  bmp_temp->printSensorDetails();

  // RTC
  Wire.begin();
  DS3231_init(DS3231_CONTROL_INTCN);

  // LoRa
  if (!LoRa.begin(915E6)) {
    Serial.println("Starting LoRa failed!");
    while (1);
  }
  
}

void loop() {
  // First we need to get all of the data from the sensors
  // List of things we need to read:
  // 1 - DHT22    - temperature, humidity, heat_index
  // 2 - BMP280   - pressure
  // 3 - RTC      - time and date
  // 4 - Battery  - battery voltage
  // 5 - Rain     - rain sensor reading

  // DHT22
  temperature = dht.readTemperature();
  humidity    = dht.readHumidity();
  heatIndex   = dht.computeHeatIndex(temperature, humidity, false);

  // BMP280
  sensors_event_t pressure_event;
  bmp_pressure -> getEvent(&pressure_event);
  pressure = pressure_event.pressure;

  // RTC
  DS3231_get(&t);
  current_date = String(t.mday) + "/" + String(t.mon) + "/" + String(t.year);
  current_time = String(t.hour) + ":" + String(t.min) + "/" + String(t.sec);

  // Battery voltage
  analogReference(AR_INTERNAL1V0);
  voltage = analogRead(ADC_BATTERY) * (battery_voltage / 1023.0);
  analogReference(AR_DEFAULT);

  // Rain sensor
  digitalWrite(MOSFET, HIGH);
  delay(100);
  rain_sensor = analogRead(A1);
  digitalWrite(MOSFET, LOW);

  /* After gathering all of the necessary data, we need to format it into our message
  // Each field is enclosed in a start flag (&S...) and end flag (&E...)
  // List of all flags:
  # Data Contained in the Sensor Message forwarded by the GCS
  # Numb  - Name          - Start         - Stop          - Description
  # 1     - Message ID    - &SID          - &EID          - ID of the message packet
  # 2     - Box ID        - &SBOXID       - &SBOXID       - ID of the sensor box - Arduino MKR WAN 1300
  # 3     - Date          - &SDATE        - &EDATE        - Date when the message was sent
  # 4     - Time          - &STIME        - &ETIME        - Timestamp when the message was sent
  # 5     - Temperature   - &STEMP        - &ETEMP        - Temperature measured by the DHT22
  # 6     - Heat Index    - &SHEATINDEX   - &EHEATINDEX   - Heat index calculated using the measurements made by the DHT22
  # 7     - Humidity      - &SHUMIDITY    - &EHUMIDITY    - Humidity measured by the DHT22
  # 8     - Pressure      - &SPRESSURE    - &EPRESSURE    - Pressure measured by the BMP280
  # 9     - Battery       - &SBAT         - &EBAT         - Voltage of the batteries powering the sensor box
  # 10    - Rain sensor   - &SRAIN        - &ERAIN        - Measurement of the Arduino Rain Sensor
  */

  // 1  - Message ID
  String msg1 = "&SID" + String(msg_counter) + "&EID";

  // 2  - Box ID
  String msg2 = "&SBOXID" + String(BOX_ID) + "&EBOXID";

  // 3  - Date
  String msg3 = "&SDATE" + current_date + "&EDATE";

  // 4  - Time
  String msg4 = "&STIME" + current_time + "&ETIME";

  // 5  - Temperature
  String msg5 = "&STEMP" + String(temperature) + "&ETEMP";

  // 6  - Heat Index
  String msg6 = "&SHEATINDEX" + String(heatIndex) + "&EHEATINDEX";

  // 7  - Humidity
  String msg7 = "&SHUMIDITY" + String(humidity) + "&EHUMIDITY";

  // 8  - Pressure
  String msg8 = "&SPRESSURE" + String(pressure) + "&EPRESSURE";

  // 9  - Battery
  String msg9 = "&SBAT" + String(voltage) + "&EBAT";

  // 10 - Rain Sensor
  String msg10 = "&SRAIN" + String(rain_sensor) + "&ERAIN";

  // To form the final message, we need to combine all 10 messages into one single message
  String lora_message = msg1 + msg2 + msg3 + msg4 + msg5 + msg6 + msg7 + msg8 + msg9 + msg10;

  // Incrementing the msg_counter
  msg_counter++;

  // Sending the LoRa message
  LoRa.beginPacket();
  LoRa.print(lora_message);
  LoRa.endPacket();

  LowPower.sleep(2000);
  
}

GCS Code

The GCS (Ground Control Station), serves as a link between my laptop and the other components of this system that include the Rover and the sensors boxes. I added a small buzzer and 2 displays to the GCS for displaying some important information or anything else we desire. The code essentially needs to work with Serial data coming from the GUI and with LoRa messages that come from the Rover and the sensor boxes and just forward them,

#include <SPI.h>
#include <LoRa.h>
#include <Wire.h>
#include "rgb_lcd.h"
#include "TM1651.h"

#define CLK 3    //pins definitions for TM1651 and can be changed to other ports       
#define DIO 4
#define BUZZER 5
TM1651 batteryDisplay(CLK,DIO);

String inputString = ""; 
bool stringComplete = false;

int batLevel = 0;
long previousTime = 0;
bool risingLevel = true;
int packetNumber = 0;

rgb_lcd lcd;

const int colorR = 200;
const int colorG = 50;
const int colorB = 150;

void setup() {
  // put your setup code here, to run once:
  pinMode(BUZZER, OUTPUT);
  Serial.begin(9600);
  lcd.begin(16, 2);
  lcd.setRGB(colorR, colorG, colorB);
  batteryDisplay.init();
  batteryDisplay.set(7);//set brightness: 0---7
  if (!LoRa.begin(915E6)) {
    Serial.println("Starting LoRa failed!");
    while (1);
  }
}

void sendLoraMessage(String message)
{
  LoRa.beginPacket();
  LoRa.print(message);
  LoRa.endPacket();
}

void loop() {
  // put your main code here, to run repeatedly:
  if(stringComplete == false)
  {
    char c = 'a';
    if(Serial.available() != 0)
    {
      c = (char)Serial.read();  
      inputString += c;
    }
    if(c == '\n')
    {
      stringComplete = true;  
    }
    
  }
  else
  {
    Serial.println(inputString);
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print(inputString.substring(0, inputString.length() - 1));
    sendLoraMessage(inputString);
    inputString = "";
    stringComplete = false;  
  }

  if(millis() - previousTime >= 1000)
  {
    if(risingLevel == true)
    {
    batLevel++;
    if(batLevel == 7) risingLevel = false;
    if(batLevel == 7) beepSiren();
    }
    else
    {
    batLevel--;  
    if(batLevel == 0) risingLevel = true;
    if(batLevel == 0) beepSiren();
    }
    batteryDisplay.displayLevel(batLevel);
    previousTime = millis();
  }
  // try to parse packet
  int packetSize = LoRa.parsePacket();
  if (packetSize) {
    // received a packet
    Serial.print("Received packet '");

    // read packet
    while (LoRa.available()) {
      char inChar = (char)LoRa.read();
      Serial.print(inChar);
      inputString += inChar;
    }

    // print RSSI of packet
    Serial.print("' with RSSI ");
    Serial.println(LoRa.packetRssi());

    packetNumber++;
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Received package with ID:");
    lcd.setCursor(0, 1);
    int pos = inputString.indexOf("&EID");
    lcd.print(inputString.substring(4,pos));
    inputString = "";
  }
  
}

void beepSiren()
{
  return;
  digitalWrite(BUZZER, HIGH);
  delay(200);
  digitalWrite(BUZZER, LOW);
  delay(100);
  digitalWrite(BUZZER, HIGH);
  delay(200);
  digitalWrite(BUZZER, LOW);
  delay(100);
}

Rover Arduino Mega Code

This is the main Arduino in the rover. The MKR WAN is used as a radio for communicating with the GCS. The reason I used the Arduino Mega is the sheer number of pins, especially the number of PWM pins. The Arduino Mega is what connects all of the things on the rover together, the 4 drive motors, all of the servos, the IMU. For now, I programmed a few basic maneuvers on the Arduino Mega like driving straight, steering the wheels, turning around, and changing the pitch of the rover body.

// Libraries
#include <Servo.h>
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
#include <Wire.h>

// Pins
// Drive Motor EN Pins
#define M1 2
#define M2 3
#define M3 4
#define M4 5

// Drive Motor Direction Pins
#define M1_IN1 22
#define M1_IN2 23
#define M2_IN1 24
#define M2_IN2 25
#define M3_IN1 26
#define M3_IN2 27
#define M4_IN1 28
#define M4_IN2 29

// Steering Servo Pins
#define S1 6
#define S2 7
#define S3 8
#define S4 9

// Steering Servo Center Positions
#define s1_center 85
#define s2_center 80
#define s3_center 95
#define s4_center 85

// Suspension & Claw Servo Pins
#define S5 10
#define S6 11

// Suspension Servo Stationary Point
#define s6_stationary 90

// All of the servos
Servo s1, s2, s3, s4, s5, s6;

// IMU
Adafruit_MPU6050 mpu;

// PI suspension regulator
float kp          = 10.0;
float ki          = 0.12;
float e           = 0.0;
float pitch       = 0.0;
float roll        = 0.0;
int servoCommand  = 0.0;
float intSum      = 0.0;

float angle_reference = 0.0;

String inputString = "";         // a String to hold incoming data
bool stringComplete = false;  // whether the string is complete

bool activeCommand = false;
long cmd_start_time = 0;
int cmd_length = 0;
bool ta_cmd = false;

int mq7_val = 7;
int mq135_val = 135;
float inside_temp = 0.0;
int msg_interval = 2000;
long previous_msg = 0;

// Function for controlling the motor direction
void setMotorDirection(int motor, int dir)
{
  // Motor can take values: 1, 2, 3, 4  - Number of the motor
  // Dir can take values: 1, 0          - 1 forward, 0 reverse
  
  if(motor != 1 && motor != 2 && motor != 3 && motor != 4)
  {
    Serial.println("Motor number doesn't exist");
    return;
    }

  if(dir != 0 && dir != 1)
  {
    Serial.println("Direction unrecognized");
    return;
    }

  switch(motor)
  {
    case 1:
      // Now we check the direction input
      if(dir == 0)
      {
        digitalWrite(M1_IN1, LOW);
        digitalWrite(M1_IN2, HIGH);
        }
        else
        {
          digitalWrite(M1_IN1, HIGH);
          digitalWrite(M1_IN2, LOW);
          }
          break;
    case 2:
      // Now we check the direction input
      if(dir == 0)
      {
        digitalWrite(M2_IN1, HIGH);
        digitalWrite(M2_IN2, LOW);
        }
        else
        {
          digitalWrite(M2_IN1, LOW);
          digitalWrite(M2_IN2, HIGH);
          }
          break;
    case 3:
      // Now we check the direction input
      if(dir == 0)
      {
        digitalWrite(M3_IN1, LOW);
        digitalWrite(M3_IN2, HIGH);
        }
        else
        {
          digitalWrite(M3_IN1, HIGH);
          digitalWrite(M3_IN2, LOW);
          }
          break;
    case 4:
      // Now we check the direction input
      if(dir == 0)
      {
        digitalWrite(M4_IN1, HIGH);
        digitalWrite(M4_IN2, LOW);
        }
        else
        {
          digitalWrite(M4_IN1, LOW);
          digitalWrite(M4_IN2, HIGH);
          }
          break;
    default:
      Serial.println("Error in motor switch");
      return;
    }
  }

// Function for controlling the motors
void motorControl(int motor, int dir, int pwr)
{
  // Motor can take values: 1, 2, 3, 4  - Number of the motor
  // Dir can take values: 1, 0          - 1 forward, 0 reverse
  // PWR can take vaues: 0 - 255        - 0 - 0%, 255 - 100%
  if(motor != 1 && motor != 2 && motor != 3 && motor != 4)
  {
    Serial.println("Motor number doesn't exist");
    return;
    }

  if(dir != 0 && dir != 1)
  {
    Serial.println("Direction unrecognized");
    return;
    }

  if(pwr < 0 || pwr > 255)
  {
    Serial.println("Motor power out of range");
    return;
    }

  setMotorDirection(motor, dir);

  switch(motor)
  {
    case 1:
      analogWrite(M1, pwr);
      break;
    case 2:
      analogWrite(M2, pwr);
      break;
    case 3:
      analogWrite(M3, pwr);
      break;
    case 4:
      analogWrite(M4, pwr);
      break;
    default:
      Serial.println("Motor Swtich Error");
      return;
    }
  
  }

// Function for clipping the servo angle to 0-180 range
int clip_angle(int center, int angle){
    if(angle + center > 180)  return 180;
    if(angle + center < 0)    return 0;
    return center + angle;
  }

// Function for steering each of the servo motors
void steerServo(int servo_motor, int angle)
{
  // Servo_Motor can take values: 1, 2, 3, 4  - Number of the motor
  // Angle can take values: -90 - 90          - 0 is the center position for the servo
  if(servo_motor != 1 && servo_motor != 2 && servo_motor != 3 && servo_motor != 4)
  {
    Serial.println("Servo motor number doesn't exist");
    return;
    }
  if(angle < -90 || angle > 90)
  {
    Serial.println("Servo angle out of range");
    return;
    }

  switch(servo_motor)
  {
    case 1:
      s1.write(clip_angle(s1_center, angle));
    case 2:
      s2.write(clip_angle(s2_center, angle));
    case 3:
      s3.write(clip_angle(s3_center, angle));
    case 4:
      s4.write(clip_angle(s4_center, angle));
    default:
      Serial.println("Servo motor switch error");
      break;
    }

  }

// Function for controlling the angle of the rover body - the active suspension
void activeSuspension(int reference)
{
  // Getting the IMU data
  sensors_event_t a, g, temp;
  mpu.getEvent(&g, &a, &temp);
  roll  = g.gyro.x;
  pitch = g.gyro.y;
  inside_temp = temp.temperature;
  
  Serial.print("Current pitch: ");
  Serial.println(pitch);

  // Calculating the error e
  e = -1 * (reference - pitch);
  intSum += e;
  servoCommand = s6_stationary + round(e * kp + ki * intSum);

  if(servoCommand > 180 || servoCommand < 0)
  {
    intSum -= e;
    servoCommand -= ki*e;
    }

  s6.write(servoCommand);
  }

// This function is triggered if the Arduino detects anything on the Serial1 port
void serialEvent1() {
  while (Serial1.available()) {
    // get the new byte:
    char inChar = (char)Serial1.read();
    // add it to the inputString:
    inputString += inChar;
    // if the incoming character is a newline, set a flag so the main loop can
    // do something about it:
    if (inChar == '\n') {
      stringComplete = true;
    }
  }
}

// Function for moving the rover
void moveRover(int dir, int motor_speed, int spin_time)
{

  if(activeCommand == false)
  {
    cmd_start_time = millis();
    cmd_length = spin_time;
    activeCommand = true;
    motorControl(1, dir, motor_speed);
    motorControl(2, dir, motor_speed);
    motorControl(3, dir, motor_speed);
    motorControl(4, dir, motor_speed);
  }
  
}

void turnRover(int dir, int motor_speed_num, int spin_time)
{
  steerServo(1, 57);
  steerServo(2, -57);
  steerServo(3, 57);
  steerServo(4, -57);

  cmd_start_time = millis();
  cmd_length = spin_time;
  activeCommand = true;
  ta_cmd = true;

  if(dir == 0)
  {
    motorControl(1, 0, motor_speed_num);
    motorControl(2, 0, motor_speed_num);
    motorControl(3, 1, motor_speed_num);
    motorControl(4, 1, motor_speed_num);
  }
  else
  {
    motorControl(1, 1, motor_speed_num);
    motorControl(2, 1, motor_speed_num);
    motorControl(3, 0, motor_speed_num);
    motorControl(4, 0, motor_speed_num);
  }

  
}

// Function that checks if the rover has been moving for as long as it has too
void stopRover()
{
  if(activeCommand == true)
  {
    if(cmd_length < millis() - cmd_start_time)
    {
      stopAllMotors();
      activeCommand = false;

      if(ta_cmd == true)
      {
        steerServo(1, 0);
        steerServo(2, 0);
        steerServo(3, 0);
        steerServo(4, 0);
        ta_cmd = false;
      }
    }
  }
  return;
}

void stopAllMotors()
{
  motorControl(1, 1, 0);
  motorControl(2, 1, 0);
  motorControl(3, 1, 0);
  motorControl(4, 1, 0);
}

// Function for extracting the command
void extractCommand(String message)
{
  if(message.indexOf("&RoverCommand") == -1)
  {
    // This is the case where we haven't received a message that is a command
    Serial.println("This is not a rover command");
    return;
  }

  int cs = message.indexOf("&CS");
  String cmd = message.substring(cs + 3, cs + 4);
  Serial.println(cmd);
  if(cmd == "X")
  {
    // This is an emergency STOP command
    stopAllMotors();
    return;
  }
  else if(cmd == "M")
  {
    // This means that we have received a move command
    Serial.println("Move command");

    // We need to extract the data from that message
    int ds = message.indexOf("&DS");
    int d1 = message.indexOf(";");
    int d2 = message.indexOf(":");
    int de = message.indexOf("&DE");

    // Extracting the data as strings
    String dir          = message.substring(ds + 3, d1);
    String motor_speed  = message.substring(d1 + 1, d2);
    String spin_time    = message.substring(d2 + 1, de);  

    // Converting all of the data to int
    int dir_num = dir.toInt();
    int motor_speed_num = motor_speed.toInt();
    int spin_time_num = spin_time.toInt();

    Serial.println(dir_num);
    Serial.println(motor_speed_num);
    Serial.println(spin_time_num);

    // Using the move command
    moveRover(dir_num, motor_speed_num, spin_time_num);
  }
  else if(cmd == "S")
  {
    int ds = message.indexOf("&DS");
    int de = message.indexOf("&DE");

    String angle_cmd =  message.substring(ds + 3, de);

    int angle_num = angle_cmd.toInt();

    steerServo(1, angle_num);
    steerServo(2, -angle_num);
    steerServo(3, -angle_num);
    steerServo(4, angle_num);

    return;
  }
  else if(cmd == "T")
  {
    // This means that we have received a move command
    Serial.println("Turn in place command");

    // We need to extract the data from that message
    int ds = message.indexOf("&DS");
    int d1 = message.indexOf(";");
    int d2 = message.indexOf(":");
    int de = message.indexOf("&DE");

    // Extracting the data as strings
    String dir          = message.substring(ds + 3, d1);
    String motor_speed  = message.substring(d1 + 1, d2);
    String spin_time    = message.substring(d2 + 1, de);  

    // Converting all of the data to int
    int dir_num = dir.toInt();
    int motor_speed_num = motor_speed.toInt();
    int spin_time_num = spin_time.toInt();

    Serial.println(dir_num);
    Serial.println(motor_speed_num);
    Serial.println(spin_time_num);
    
    turnRover(dir_num, motor_speed_num, spin_time_num);
  }
  else if(cmd == "P")
  {
    // This means that we have received a move command
    Serial.println("Turn in place command");

    // We need to extract the data from that message
    int ds = message.indexOf("&DS");
    int de = message.indexOf("&DE");

    String angle_ref = message.substring(ds + 3, de);

    angle_reference = angle_ref.toFloat();
  }
  else
  {
    Serial.println("Unrecognized command");
  }
}

void sendToGUI()
{
  if(millis() - previous_msg > msg_interval)
  {
    previous_msg = millis();

    mq7_val = analogRead(A0);
    mq135_val = analogRead(A1);
    
    String msg1 = "&SID243&EID";
    String msg2 = "&SBOXID3&EBOXID";
    String msg3 = "&STEMP" + String(inside_temp) + "&ETEMP";
    String msg4 = "&SMQ7" + String(mq7_val) + "&EMQ7";
    String msg5 = "&SMQ135" + String(mq135_val) + "&EMQ135";
    String msg6 = "&SPITCH" + String(pitch) + "&EPITCH";
    String msg7 = "&SROLL" + String(roll) + "&EROLL";

    String full_message = msg1 + msg2 + msg3 + msg4 + msg5 + msg6 + msg7;

    Serial1.println(full_message);
  }
  }

void setup() {

  // Serial
  Serial.begin(9600);
  Serial1.begin(9600);
  inputString.reserve(200); // This is for the message that comes from the Arduino MKR WAN 1300

  // Setting up the pins
  // Motor 1 pins
  pinMode(M1,     OUTPUT);
  pinMode(M1_IN1, OUTPUT);
  pinMode(M1_IN2, OUTPUT);

  // Motor 2 pins
  pinMode(M2,     OUTPUT);
  pinMode(M2_IN1, OUTPUT);
  pinMode(M2_IN2, OUTPUT);

  // Motor 3 pins
  pinMode(M3,     OUTPUT);
  pinMode(M3_IN1, OUTPUT);
  pinMode(M3_IN2, OUTPUT);

  // Motor 4 pins
  pinMode(M4,     OUTPUT);
  pinMode(M4_IN1, OUTPUT);
  pinMode(M4_IN2, OUTPUT);

  // Attaching the Sevos
  s1.attach(S1);
  s2.attach(S2);
  s3.attach(S3);
  s4.attach(S4);
  s5.attach(S5);
  s6.attach(S6);

  // Centering the servos
  steerServo(1, 0);
  steerServo(2, 0);
  steerServo(3, 0);
  steerServo(4, 0);

  // Putting the suspension servo in stationary
  s6.write(s6_stationary);

  // Putting all of the motors to forward and shutting them down
  motorControl(1, 1, 0);
  motorControl(2, 1, 0);
  motorControl(3, 1, 0);
  motorControl(4, 1, 0);

  // Try to initialize the IMU
  if (!mpu.begin()) {
    Serial.println("Failed to find MPU6050 chip");
    while (1) {
      delay(10);
    }
  }
  
  mpu.setAccelerometerRange(MPU6050_RANGE_16_G);
  mpu.setGyroRange(MPU6050_RANGE_250_DEG);
  mpu.setFilterBandwidth(MPU6050_BAND_21_HZ);

  delay(2000);

  previous_msg = millis();
 
}

void loop() {

  activeSuspension(angle_reference);
  sendToGUI();
  stopRover();

  if (stringComplete) {
    Serial.println(inputString);
    extractCommand(inputString);
    // clear the string:
    inputString = "";
    stringComplete = false;
  }
    
  delay(10);
  
}

Rover Arduino MKR WAN Code

This Arduino functions as a link between the Arduino Mega and the GCS.

#include <SPI.h>
#include <LoRa.h>

String inputString = ""; 
bool stringComplete = false;

void setup() {
  Serial1.begin(9600);

  if (!LoRa.begin(915E6)) {
    while (1);
  }
}

void sendLoraMessage(String message)
{
  LoRa.beginPacket();
  LoRa.print(message);
  LoRa.endPacket();
}

void loop() {
  // try to parse packet
  int packetSize = LoRa.parsePacket();
  if (packetSize) {
    // received a packet
    Serial1.print("Received packet '");

    // read packet
    while (LoRa.available()) {
      Serial1.print((char)LoRa.read());
    }

    // print RSSI of packet
    Serial1.print("' with RSSI ");
    Serial1.println(LoRa.packetRssi());
  }

  if(stringComplete == false)
  {
    char c = 'a';
    if(Serial1.available() != 0)
    {
      c = (char)Serial1.read();  
      inputString += c;
    }
    if(c == '\n')
    {
      stringComplete = true;  
    }
    
  }
  else
  {
    Serial.println(inputString);
    sendLoraMessage(inputString);
    inputString = "";
    stringComplete = false;  
  }




  
}

GUI Code

This is by far the thing that took the most time. The GUI consists of 2 windows where one is dedicated to the data from the sensors boxes, while the other window has data from the rover as well as commands for the rover. I wrote the whole GUI in python using PyQt. Besides just showing all of the data on live graphs, the GUI also collects all of the data and saves it into CSV files so we can more easily work with the data later on. The GUI communicates with the system over a COM port, more specifically, it communicates with the GCS.

# PYQT Stuff
from PyQt5.QtWidgets import QApplication, QPushButton, QLineEdit, QLabel, QWidget, QSlider, QSpinBox, QComboBox, QMainWindow
from PyQt5.QtGui import QPainter, QColor, QPen, QFont, QBrush
from PyQt5.QtCore import Qt, QTimer
from PyQt5 import QtWidgets
from pyqtgraph.Qt import QtGui, QtCore
from pyqtgraph import PlotWidget, plot
import pyqtgraph as pg

# Libraries
import csv
import datetime
import os
import sys
import threading
import time
import serial
from random import randint


# Design Variables
borderColor = QColor(180, 180, 180)
borderWidth = 3

gcs = serial.Serial("COM3", 9600)

print("Writing to Arduino")

# Data Contained in the Sensor Message forwarded by the GCS
# Numb  - Name          - Start         - Stop          - Description
# 1     - Message ID    - &SID          - &EID          - ID of the message packet
# 2     - Box ID        - &SBOXID       - &EBOXID       - ID of the sensor box - Arduino MKR WAN 1300
# 3     - Date          - &SDATE        - &EDATE        - Date when the message was sent
# 4     - Time          - &STIME        - &ETIME        - Timestamp when the message was sent
# 5     - Temperature   - &STEMP        - &ETEMP        - Temperature measured by the DHT22
# 6     - Heat Index    - &SHEADINDEX   - &EHEATINDEX   - Heat index calculated using the measurements made by the DHT22
# 7     - Humidity      - &SHUMIDITY    - &EHUMIDITY    - Humidity measured by the DHT22
# 8     - Pressure      - &SPRESSURE    - &EPRESSURE    - Pressure measured by the BMP280
# 9     - Battery       - &SBAT         - &EBAT         - Voltage of the batteries powering the sensor box
# 10    - Rain sensor   - &SRAIN        - &ERAIN        - Measurement of the Arduino Rain Sensor

# Variables for storing all of the data from the GCS
message_id  = 0
box_id  	= 0
msg_date    = ''
msg_time    = ''
temperature = 0.0
heat_index  = 0.0
humidity    = 0.0
pressure    = 0.0
battery     = 0.0
rain_sensor = 0

batch_size = 700000
sb1_temp = []
sb1_humidity  = []
sb1_pressure = []
sb1_battery = []
sb1_heatindex = []
sb1_time = []

sb2_temp = []
sb2_humidity  = []
sb2_pressure = []
sb2_battery = []
sb2_heatindex = []
sb2_time = []

rover_temp = []
rover_roll = []
rover_pitch = []
rover_mq7 = []
rover_mq135 = []
rover_time = []

flags = ['ID', 'BOXID', 'DATE', 'TIME', 'TEMP', 'HEATINDEX', 'HUMIDITY', 'PRESSURE', 'BAT', 'RAIN']

# Rover commands
cmd_steering_angle = 0
cmd_pitch_angle = 0.0
move_dir = 1
move_speed = 0
move_time = 0
turn_dir = 1
turn_speed = 0
turn_time = 0

def is_float(number):
    try:
        float(number)
        return True
    except ValueError:
        return False

# Function for logging the data into a CSV file
def log_data(data, id):

    file_name = 'SensorBox' + str(id) + '.csv'
    file_path = './' + file_name

    if os.path.isfile(file_path) == False:
        print('The CSV file for the box ' + str(id) + ' doesn\'t exist, trying to create it')
        print('Name of the file: ' + file_path)

        with open(file_path, mode = 'w', newline = '') as log_file:
            writer = csv.writer(log_file)
            writer.writerow(['MESSAGE_ID', 'DATE', 'TIME', 'TEMP', 'HEATINDEX', 'HUMIDITY', 'PRESSURE', 'BAT', 'RAIN'])

    with open(file_path, mode = 'a', newline = '') as log_file:
        writer = csv.writer(log_file)
        writer.writerow(data)

    return

count1 = 0
count2 = 0
count3 = 0

def parseRoverData(data):
    
    global rover_temp, rover_roll, rover_pitch, rover_mq7, rover_mq135, rover_time, count3
    
    rover_temp.append(float(data[data.find('&STEMP') + len('&STEMP') : data.find('&ETEMP')]))
    rover_roll.append(float(data[data.find('&SROLL') + len('&SROLL') : data.find('&EROLL')]))
    rover_pitch.append(float(data[data.find('&SPITCH') + len('&SPITCH') : data.find('&EPITCH')]))
    rover_mq7.append(int(data[data.find('&SMQ7') + len('&SMQ7') : data.find('&EMQ7')]))
    rover_mq135.append(int(data[data.find('&SMQ135') + len('&SMQ135') : data.find('&EMQ135')]))
    rover_time.append(count3)
    count3 += 1
    
    print('Rover data')
    print('-------------------------------------------------')
    print('Raw data: ' + data)
    print('Temperature: ' + str(rover_temp[-1]))
    print('Roll: ' + str(rover_roll[-1]))
    print('Pitch: ' + str(rover_pitch[-1]))
    print('MQ7: ' + str(rover_mq7[-1]))
    print('MQ135: ' + str(rover_mq135[-1]))
    print('-------------------------------------------------')

    

# Function for parsing the Sensor Box data
def parseData(data):
    global message_id, box_id, msg_date, msg_time, temperature, heat_index, humidity, pressure, battery, rain_sensor

    global flags

    id_box = 0
    csv_data = []
    
    if data.find('&SBOXID') == -1:
        return

    for flag in flags:
        start_flag  = '&S' + flag
        end_flag    = '&E' + flag
        extracted_data  = data[data.find(start_flag) + len(start_flag) : data.find(end_flag)]
        print(flag + ':\t' + extracted_data)

        if flag == 'BOXID':
            #id_box = 1
            id_box = int(extracted_data)
        else:
            csv_data.append(extracted_data)
            
        global count1, count2
        
        if id_box == 1:
            global sb1_temp, sb1_time, sb1_humidity, sb1_pressure, sb1_battery, sb1_heatindex
            
            if flag == 'TEMP' and is_float(extracted_data) == True:
                sb1_temp.append(float(extracted_data))
                sb1_time.append(count1)
                count1 += 1
                
            if flag == 'HEATINDEX' and is_float(extracted_data) == True:
                sb1_heatindex.append(float(extracted_data))
                
            if flag == 'HUMIDITY' and is_float(extracted_data) == True:
                sb1_humidity.append(float(extracted_data))
                
            if flag == 'PRESSURE' and is_float(extracted_data) == True:
                sb1_pressure.append(float(extracted_data))
                
            if flag == 'BAT' and is_float(extracted_data) == True:
                sb1_battery.append(float(extracted_data))
        elif id_box == 2:
            global sb2_temp, sb2_time, sb2_humidity, sb2_pressure, sb2_battery, sb2_heatindex
            
            if flag == 'TEMP' and is_float(extracted_data) == True:
                sb2_temp.append(float(extracted_data))
                sb2_time.append(count2)
                count2 += 1
                
            if flag == 'HEATINDEX' and is_float(extracted_data) == True:
                sb2_heatindex.append(float(extracted_data))
                
            if flag == 'HUMIDITY' and is_float(extracted_data) == True:
                sb2_humidity.append(float(extracted_data))
                
            if flag == 'PRESSURE' and is_float(extracted_data) == True:
                sb2_pressure.append(float(extracted_data))
                
            if flag == 'BAT' and is_float(extracted_data) == True:
                sb2_battery.append(float(extracted_data))
        elif id_box == 3:
            print('Received message from Rover!!!!')
            print(data)
            parseRoverData(data)
            break

    print('----------------------------------------------------------------------------------------------')
    log_data(csv_data, id_box)

class RoverWindow(QWidget):
    
    global cmd_steering_angle, cmd_pitch_angle
    
    def __init__(self):       
        super().__init__()
        self.title = 'Rover Command'
        self.left = 200
        self.top = 200
        self.width = 1920
        self.height = 1080
        self.qTimer = QTimer()
        self.qTimer.setInterval(50)    
        self.qTimer.start()
        self.qTimer.timeout.connect(self.update_plot_data)
        self.setWindowIcon(QtGui.QIcon('./Orbweaver.png'))
        self.initUI()
         
    def initUI(self):
        self.setWindowTitle(self.title)
        self.setGeometry(self.left, self.top, self.width, self.height) 
        self.setAutoFillBackground(True)
        p = self.palette()
        p.setColor(self.backgroundRole(), QColor(25, 35, 40))
        self.setPalette(p)
        
        #
        # Graph widgets for the Rover
        #
        
        # Temperature inside the rover
        self.label_sb1_temperature = QLabel(self)
        self.label_sb1_temperature.setText("Temperature inside Rover")
        self.label_sb1_temperature.setGeometry(20, 17, 420, 20)
        self.label_sb1_temperature.setFont(QFont("Arial", 18))
        self.label_sb1_temperature.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        self.label_sb1_temperature.setAlignment(QtCore.Qt.AlignCenter)
        
        self.graphWidget_rover_temperature = pg.PlotWidget(self)
        self.graphWidget_rover_temperature.setGeometry(20, 50, 440, 350)
        self.graphWidget_rover_temperature.setBackground(None)
        self.graphWidget_rover_temperature.setYRange(-20, 15, padding=0.04)
        
        data1_pen = pg.mkPen(color=(255, 7, 58), width=3)
        self.dataLine_rover_temperature = self.graphWidget_rover_temperature.plot(rover_time, rover_temp, pen = data1_pen)
        
        # IMU
        self.label_sb1_humidity = QLabel(self)
        self.label_sb1_humidity.setText("Pitch & Roll")
        self.label_sb1_humidity.setGeometry(490, 17, 440, 20)
        self.label_sb1_humidity.setFont(QFont("Arial", 18))
        self.label_sb1_humidity.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        self.label_sb1_humidity.setAlignment(QtCore.Qt.AlignCenter)
        
        self.graphWidget_rover_imu = pg.PlotWidget(self)
        self.graphWidget_rover_imu.setGeometry(490, 50, 440, 350)
        self.graphWidget_rover_imu.setBackground(None)
        self.graphWidget_rover_imu.setYRange(0, 100, padding=0.04)
        
        data1_pen = pg.mkPen(color=(255, 237, 39), width=3)
        self.dataLine_rover_pitch = self.graphWidget_rover_imu.plot(rover_time, rover_pitch, pen = data1_pen)
        
        data2_pen = pg.mkPen(color=(255, 95, 31), width=3)
        self.dataLine_rover_roll = self.graphWidget_rover_imu.plot(rover_time, rover_roll, pen = data2_pen)
        
        
        # MQ 7
        self.label_sb1_pressure = QLabel(self)
        self.label_sb1_pressure.setText("MQ 7")
        self.label_sb1_pressure.setGeometry(20, 412, 420, 20)
        self.label_sb1_pressure.setFont(QFont("Arial", 18))
        self.label_sb1_pressure.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        self.label_sb1_pressure.setAlignment(QtCore.Qt.AlignCenter)
        
        self.graphWidget_rover_mq7 = pg.PlotWidget(self)
        self.graphWidget_rover_mq7.setGeometry(20, 443, 440, 350)
        self.graphWidget_rover_mq7.setBackground(None)
        self.graphWidget_rover_mq7.setYRange(900, 1100, padding=0.04)
        
        data1_pen = pg.mkPen(color=(0, 200, 255), width=3)
        self.dataLine_rover_mq7 = self.graphWidget_rover_mq7.plot(rover_time, rover_mq7, pen = data1_pen)
        
        # MQ 135
        self.label_sb1_battery_level = QLabel(self)
        self.label_sb1_battery_level.setText("MQ 135")
        self.label_sb1_battery_level.setGeometry(490, 412, 440, 20)
        self.label_sb1_battery_level.setFont(QFont("Arial", 18))
        self.label_sb1_battery_level.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        self.label_sb1_battery_level.setAlignment(QtCore.Qt.AlignCenter)
        
        self.graphWidget_rover_mq135 = pg.PlotWidget(self)
        self.graphWidget_rover_mq135.setGeometry(490, 443, 440, 350)
        self.graphWidget_rover_mq135.setBackground(None)
        self.graphWidget_rover_mq135.setYRange(2.4, 2.9, padding=0.04)
        
        data1_pen = pg.mkPen(color=(57, 255, 20), width=3)
        self.dataLine_rover_mq135 = self.graphWidget_rover_mq135.plot(rover_time, rover_mq135, pen = data1_pen)
        
        
        
        #
        # Widgets for the Rover Commands
        #
        
        # Move Command
        self.label_sb2_temperature = QLabel(self)
        self.label_sb2_temperature.setText("Move Command")
        self.label_sb2_temperature.setGeometry(980, 17, 440, 20)
        self.label_sb2_temperature.setFont(QFont("Arial", 18))
        self.label_sb2_temperature.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        self.label_sb2_temperature.setAlignment(QtCore.Qt.AlignCenter)
        
        
        #self.graphWidget_sb2_temperature.setGeometry(980, 50, 440, 350)
        
        self.btnSendSteerCmd = QPushButton("Direction", self)
        self.btnSendSteerCmd.resize(130, 50)
        self.btnSendSteerCmd.move(980, 50)
        self.btnSendSteerCmd.clicked.connect(self.btnMoveCmdFunction)
        self.btnSendSteerCmd.setFont(QFont("Arial", 20))
        self.btnSendSteerCmd.setStyleSheet("QPushButton {background-color : rgb(45, 50, 60); color : rgb(150, 150, 150); font-weight: bold}")
        
        self.label_mov_dir = QLabel(self)
        self.label_mov_dir.setText("Forward       Reverse")
        self.label_mov_dir.setGeometry(1090, 60, 300, 22)
        self.label_mov_dir.setFont(QFont("Arial", 20))
        self.label_mov_dir.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        self.label_mov_dir.setAlignment(QtCore.Qt.AlignCenter)
        
        self.label_mov_speed = QLabel(self)
        self.label_mov_speed.setText("Motor Speed")
        self.label_mov_speed.setGeometry(1050, 110, 300, 22)
        self.label_mov_speed.setFont(QFont("Arial", 20))
        self.label_mov_speed.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        self.label_mov_speed.setAlignment(QtCore.Qt.AlignCenter)
        
        self.moveSpeedSlider = QSlider(Qt.Horizontal, self)
        self.moveSpeedSlider.setGeometry(990, 140, 430, 30)
        self.moveSpeedSlider.setRange(0,255)
        self.moveSpeedSlider.valueChanged[int].connect(self.changeMoveSpeed)
        
        self.label_mov_speed = QLabel(self)
        self.label_mov_speed.setText("Speed:")
        self.label_mov_speed.setGeometry(990, 180, 100, 25)
        self.label_mov_speed.setFont(QFont("Arial", 20))
        self.label_mov_speed.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        self.label_mov_speed.setAlignment(QtCore.Qt.AlignCenter)
        
        self.label_mov_speed_val = QLabel(self)
        self.label_mov_speed_val.setText("00")
        self.label_mov_speed_val.setGeometry(1070, 180, 100, 22)
        self.label_mov_speed_val.setFont(QFont("Arial", 20))
        self.label_mov_speed_val.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        self.label_mov_speed_val.setAlignment(QtCore.Qt.AlignCenter)
        
        
        
        self.label_mov_speed = QLabel(self)
        self.label_mov_speed.setText("Move Time [ms]")
        self.label_mov_speed.setGeometry(1050, 220, 300, 22)
        self.label_mov_speed.setFont(QFont("Arial", 20))
        self.label_mov_speed.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        self.label_mov_speed.setAlignment(QtCore.Qt.AlignCenter)
        
        self.moveSpeedSlider = QSlider(Qt.Horizontal, self)
        self.moveSpeedSlider.setGeometry(990, 250, 430, 30)
        self.moveSpeedSlider.setRange(0,10000)
        self.moveSpeedSlider.valueChanged[int].connect(self.changeMoveTime)
        
        self.label_mov_time = QLabel(self)
        self.label_mov_time.setText("Time:")
        self.label_mov_time.setGeometry(990, 290, 100, 25)
        self.label_mov_time.setFont(QFont("Arial", 20))
        self.label_mov_time.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        self.label_mov_time.setAlignment(QtCore.Qt.AlignCenter)
        
        self.label_mov_time_val = QLabel(self)
        self.label_mov_time_val.setText("00")
        self.label_mov_time_val.setGeometry(1070, 290, 100, 22)
        self.label_mov_time_val.setFont(QFont("Arial", 20))
        self.label_mov_time_val.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        self.label_mov_time_val.setAlignment(QtCore.Qt.AlignCenter)
        
        self.btnSendSteerCmd = QPushButton("Send Move Command", self)
        self.btnSendSteerCmd.resize(350, 50)
        self.btnSendSteerCmd.move(1020, 330)
        self.btnSendSteerCmd.clicked.connect(self.btnSendMoveCmdFunction)
        self.btnSendSteerCmd.setFont(QFont("Arial", 20))
        self.btnSendSteerCmd.setStyleSheet("QPushButton {background-color : rgb(45, 50, 60); color : rgb(150, 150, 150); font-weight: bold}")
        
        
        
        
        
        # Steer Command
        self.label_sb2_humidity = QLabel(self)
        self.label_sb2_humidity.setText("Steer Command")
        self.label_sb2_humidity.setGeometry(1450, 17, 440, 20)
        self.label_sb2_humidity.setFont(QFont("Arial", 18))
        self.label_sb2_humidity.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        self.label_sb2_humidity.setAlignment(QtCore.Qt.AlignCenter)
        #self.graphWidget_sb2_humidity.setGeometry(1450, 50, 440, 350).
        self.label_angle = QLabel(self)
        self.label_angle.setText("Steering Angle")
        self.label_angle.setGeometry(1450, 80, 440, 40)
        self.label_angle.setFont(QFont("Arial", 25))
        self.label_angle.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        self.label_angle.setAlignment(QtCore.Qt.AlignCenter)
        # Slider for picking the angle
        self.angleSlider = QSlider(Qt.Horizontal, self)
        self.angleSlider.setGeometry(1450, 150, 430, 30)
        self.angleSlider.setRange(-90,90)
        self.angleSlider.valueChanged[int].connect(self.changeSliderValue)
        #self.Slider1.valueChanged[int].connect(self.changeSliderValue)
        self.label_angle1 = QLabel(self)
        self.label_angle1.setText("Angle: ")
        self.label_angle1.setGeometry(1450, 230, 100, 35)
        self.label_angle1.setFont(QFont("Arial", 22))
        self.label_angle1.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        self.label_angle1.setAlignment(QtCore.Qt.AlignCenter)
        
        self.label_angle1 = QLabel(self)
        self.label_angle1.setText("00")
        self.label_angle1.setGeometry(1510, 230, 100, 35)
        self.label_angle1.setFont(QFont("Arial", 22))
        self.label_angle1.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        self.label_angle1.setAlignment(QtCore.Qt.AlignCenter)
        
        self.btnSendSteerCmd = QPushButton("Send Steering Command", self)
        self.btnSendSteerCmd.resize(350, 50)
        self.btnSendSteerCmd.move(1500, 300)
        self.btnSendSteerCmd.clicked.connect(self.btnSteerCmdFunction)
        self.btnSendSteerCmd.setFont(QFont("Arial", 20))
        self.btnSendSteerCmd.setStyleSheet("QPushButton {background-color : rgb(45, 50, 60); color : rgb(150, 150, 150); font-weight: bold}")
        #self.btnSendSteerCmd.setStyleSheet("QPushButton {background-color : red; color : yellow; font-weight: bold}")
        
        
        
        
        # Turn Around Command
        self.label_sb2_pressure = QLabel(self)
        self.label_sb2_pressure.setText("Turn Command")
        self.label_sb2_pressure.setGeometry(970, 412, 440, 20)
        self.label_sb2_pressure.setFont(QFont("Arial", 18))
        self.label_sb2_pressure.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        self.label_sb2_pressure.setAlignment(QtCore.Qt.AlignCenter)
        
        #self.graphWidget_sb2_pressure.setGeometry(970, 443, 440, 350)
        
        self.btnSendSteerCmd = QPushButton("Direction", self)
        self.btnSendSteerCmd.resize(130, 50)
        self.btnSendSteerCmd.move(980, 450)
        self.btnSendSteerCmd.clicked.connect(self.btnTurnCmdFunction)
        self.btnSendSteerCmd.setFont(QFont("Arial", 20))
        self.btnSendSteerCmd.setStyleSheet("QPushButton {background-color : rgb(45, 50, 60); color : rgb(150, 150, 150); font-weight: bold}")
        
        self.label_mov_dir = QLabel(self)
        self.label_mov_dir.setText("Left        Right")
        self.label_mov_dir.setGeometry(1090, 460, 300, 22)
        self.label_mov_dir.setFont(QFont("Arial", 20))
        self.label_mov_dir.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        self.label_mov_dir.setAlignment(QtCore.Qt.AlignCenter)
        
        self.label_mov_speed = QLabel(self)
        self.label_mov_speed.setText("Motor Speed")
        self.label_mov_speed.setGeometry(1050, 510, 300, 22)
        self.label_mov_speed.setFont(QFont("Arial", 20))
        self.label_mov_speed.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        self.label_mov_speed.setAlignment(QtCore.Qt.AlignCenter)
        
        self.moveSpeedSlider = QSlider(Qt.Horizontal, self)
        self.moveSpeedSlider.setGeometry(990, 540, 430, 30)
        self.moveSpeedSlider.setRange(0,255)
        self.moveSpeedSlider.valueChanged[int].connect(self.changeTurnSpeed)
        
        self.label_mov_speed = QLabel(self)
        self.label_mov_speed.setText("Speed:")
        self.label_mov_speed.setGeometry(990, 580, 100, 25)
        self.label_mov_speed.setFont(QFont("Arial", 20))
        self.label_mov_speed.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        self.label_mov_speed.setAlignment(QtCore.Qt.AlignCenter)
        
        self.label_turn_speed_val = QLabel(self)
        self.label_turn_speed_val.setText("00")
        self.label_turn_speed_val.setGeometry(1070, 580, 100, 22)
        self.label_turn_speed_val.setFont(QFont("Arial", 20))
        self.label_turn_speed_val.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        self.label_turn_speed_val.setAlignment(QtCore.Qt.AlignCenter)
        
        
        
        self.label_mov_speed = QLabel(self)
        self.label_mov_speed.setText("Move Time [ms]")
        self.label_mov_speed.setGeometry(1050, 620, 300, 22)
        self.label_mov_speed.setFont(QFont("Arial", 20))
        self.label_mov_speed.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        self.label_mov_speed.setAlignment(QtCore.Qt.AlignCenter)
        
        self.moveSpeedSlider = QSlider(Qt.Horizontal, self)
        self.moveSpeedSlider.setGeometry(990, 650, 430, 30)
        self.moveSpeedSlider.setRange(0,10000)
        self.moveSpeedSlider.valueChanged[int].connect(self.changeTurnTime)
        
        self.label_mov_time = QLabel(self)
        self.label_mov_time.setText("Time:")
        self.label_mov_time.setGeometry(990, 690, 100, 25)
        self.label_mov_time.setFont(QFont("Arial", 20))
        self.label_mov_time.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        self.label_mov_time.setAlignment(QtCore.Qt.AlignCenter)
        
        self.label_turn_time_val = QLabel(self)
        self.label_turn_time_val.setText("00")
        self.label_turn_time_val.setGeometry(1070, 690, 100, 22)
        self.label_turn_time_val.setFont(QFont("Arial", 20))
        self.label_turn_time_val.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        self.label_turn_time_val.setAlignment(QtCore.Qt.AlignCenter)
        
        self.btnSendSteerCmd = QPushButton("Send Turn Command", self)
        self.btnSendSteerCmd.resize(350, 50)
        self.btnSendSteerCmd.move(1020, 730)
        self.btnSendSteerCmd.clicked.connect(self.btnSendTurnCmd)
        self.btnSendSteerCmd.setFont(QFont("Arial", 20))
        self.btnSendSteerCmd.setStyleSheet("QPushButton {background-color : rgb(45, 50, 60); color : rgb(150, 150, 150); font-weight: bold}")
        
         
        
        # Pitch Command
        self.label_sb2_battery_level = QLabel(self)
        self.label_sb2_battery_level.setText("Pitch Command")
        self.label_sb2_battery_level.setGeometry(1450, 412, 440, 20)
        self.label_sb2_battery_level.setFont(QFont("Arial", 18))
        self.label_sb2_battery_level.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        self.label_sb2_battery_level.setAlignment(QtCore.Qt.AlignCenter)
        
        #self.graphWidget_sb2_battery_level.setGeometry(1450, 443, 440, 350)
        
        self.label_angle = QLabel(self)
        self.label_angle.setText("Rover Angle - Pitch")
        self.label_angle.setGeometry(1450, 490, 440, 40)
        self.label_angle.setFont(QFont("Arial", 25))
        self.label_angle.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        self.label_angle.setAlignment(QtCore.Qt.AlignCenter)
        # Slider for picking the angle
        self.angleSliderPitch = QSlider(Qt.Horizontal, self)
        self.angleSliderPitch.setGeometry(1450, 560, 430, 30)
        self.angleSliderPitch.setRange(-90,90)
        self.angleSliderPitch.valueChanged[int].connect(self.changeSliderValuePitch)
        #self.Slider1.valueChanged[int].connect(self.changeSliderValue)
        self.label_angle11 = QLabel(self)
        self.label_angle11.setText("Angle: ")
        self.label_angle11.setGeometry(1450, 600, 100, 35)
        self.label_angle11.setFont(QFont("Arial", 22))
        self.label_angle11.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        self.label_angle11.setAlignment(QtCore.Qt.AlignCenter)
        
        self.label_angle_body = QLabel(self)
        self.label_angle_body.setText("00")
        self.label_angle_body.setGeometry(1510, 600, 100, 35)
        self.label_angle_body.setFont(QFont("Arial", 22))
        self.label_angle_body.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        self.label_angle_body.setAlignment(QtCore.Qt.AlignCenter)
        
        self.btnSendSteerCmd = QPushButton("Send Pitch Command", self)
        self.btnSendSteerCmd.resize(350, 50)
        self.btnSendSteerCmd.move(1500, 700)
        self.btnSendSteerCmd.clicked.connect(self.btnPitchCmdFunction)
        self.btnSendSteerCmd.setFont(QFont("Arial", 20))
        self.btnSendSteerCmd.setStyleSheet("QPushButton {background-color : rgb(45, 50, 60); color : rgb(150, 150, 150); font-weight: bold}")
        #self.btnSendSteerCmd.setStyleSheet("QPushButton {background-color : red; color : yellow; font-weight: bold}")
        
        
        self.show()     
        
        
    def update_plot_data(self):
        if len(sb1_time) > batch_size:
            
            self.dataLine_rover_temperature.setData(rover_time[len(rover_time) - batch_size:],rover_temp[len(rover_time) - batch_size:])
            self.dataLine_rover_roll.setData(rover_time[len(rover_time) - batch_size:],rover_roll[len(rover_time) - batch_size:])
            self.dataLine_rover_pitch.setData(rover_time[len(rover_time) - batch_size:],rover_pitch[len(rover_time) - batch_size:])
            self.dataLine_rover_mq7.setData(rover_time[len(rover_time) - batch_size:],rover_mq7[len(rover_time) - batch_size:])
            self.dataLine_rover_mq135.setData(rover_time[len(rover_time) - batch_size:],rover_mq135[len(rover_time) - batch_size:])
            
        else:
            
            self.dataLine_rover_temperature.setData(rover_time, rover_temp)
            self.dataLine_rover_roll.setData(rover_time, rover_roll)
            self.dataLine_rover_pitch.setData(rover_time, rover_pitch)
            self.dataLine_rover_mq7.setData(rover_time, rover_mq7)
            self.dataLine_rover_mq135.setData(rover_time, rover_mq135)
            
            
        
        
        
    def paintEvent(self, event):
        
        global borderColor, borderWidth, move_dir, turn_dir
        
        painter = QPainter()
        painter.begin(self)
        painter.setRenderHint(QPainter.Antialiasing)
        
        # Pen for drawing the sensor box borders
        painter.setPen(QPen(borderColor, borderWidth))
        
        # Main sensor box borders
        painter.drawRect(10, 10, 940, 1000)
        painter.drawRect(970, 10, 940, 1000)
        
        # Sensor box input data retangles
        painter.drawRect(10, 800, 940, 210)
        painter.drawRect(970, 800, 940, 210)
        
        # Drawing the graph separators
        painter.drawLine(475, 10, 475, 800)
        painter.drawLine(10, 405, 945, 405)
        painter.drawLine(650, 800, 650, 1010)
        painter.drawLine(1430, 10, 1430, 800)
        painter.drawLine(970, 405, 1910, 405)
        painter.drawLine(1620, 800, 1620, 1010)
        
        painter.setPen(QPen(borderColor, borderWidth / 2))
        
        painter.drawLine(10, 40, 945, 40)
        painter.drawLine(970, 40, 1910, 40)
        painter.drawLine(10, 435, 945, 435)
        painter.drawLine(970, 435, 1910, 435)
        
        painter.setBrush(QBrush(Qt.transparent, Qt.SolidPattern))
        if move_dir == 0:
            painter.drawEllipse(1225, 60, 20, 20)
            painter.setBrush(QBrush(QColor(57, 255, 20), Qt.SolidPattern))
            painter.drawEllipse(1380, 60, 20, 20)
        elif move_dir == 1:
            painter.drawEllipse(1380, 60, 20, 20)
            painter.setBrush(QBrush(QColor(57, 255, 20), Qt.SolidPattern))
            painter.drawEllipse(1225, 60, 20, 20)
            
        painter.setBrush(QBrush(Qt.transparent, Qt.SolidPattern))
        if turn_dir == 1:
            painter.drawEllipse(1225, 460, 20, 20)
            painter.setBrush(QBrush(QColor(57, 255, 20), Qt.SolidPattern))
            painter.drawEllipse(1380, 460, 20, 20)
        elif turn_dir == 0:
            painter.drawEllipse(1380, 460, 20, 20)
            painter.setBrush(QBrush(QColor(57, 255, 20), Qt.SolidPattern))
            painter.drawEllipse(1225, 460, 20, 20)
            
            
        
        
    def changeSliderValue(self, value):
        global cmd_steering_angle
        cmd_steering_angle = value
        #print(str(value))
        self.label_angle1.setText(str(value))
        #arduino.write(throttleSet.encode())
        
    def btnSteerCmdFunction(self):
        global cmd_steering_angle
        msg = '&RoverCommand&CSS&CE&DS' + str(cmd_steering_angle) + '&DE\n'
        print('Sending command to rover: ' + msg)
        gcs.write(msg.encode())
        
    def changeSliderValuePitch(self, value):
        global cmd_pitch_angle
        cmd_pitch_angle = value / 9
        self.label_angle_body.setText(str(value))
        
    def btnPitchCmdFunction(self):
        global cmd_pitch_angle
        msg = '&RoverCommand&CSP&CE&DS' + str(cmd_pitch_angle) + '&DE\n'
        print('Sending command to rover: ' + msg)
        gcs.write(msg.encode())
        
    def changeMoveSpeed(self, value):
        global move_speed
        move_speed = value
        self.label_mov_speed_val.setText(str(value))
        
    def changeMoveTime(self, value):
        global move_time
        move_time = value
        self.label_mov_time_val.setText(str(value))
        
    def changeTurnSpeed(self, value):
        global turn_speed
        turn_speed = value
        self.label_turn_speed_val.setText(str(value))
        
    def changeTurnTime(self, value):
        global turn_time
        turn_time = value
        self.label_turn_time_val.setText(str(value))
        
    def btnTurnCmdFunction(self):
        global turn_dir
        
        if turn_dir == 1:
            turn_dir = 0
            print(turn_dir)
            self.update()
            return
        
        if turn_dir == 0:
            turn_dir = 1
            print(turn_dir)
            self.update()
            return
        
    def btnMoveCmdFunction(self):
        global move_dir
        
        if move_dir == 1:
            move_dir = 0
            print(move_dir)
            self.update()
            return
        
        if move_dir == 0:
            move_dir = 1
            print(move_dir)
            self.update()
            return
        
    def btnSendMoveCmdFunction(self):
        global move_dir, move_time, move_speed
        msg = '&RoverCommand&CSM&CE&DS' + str(move_dir) + ';' + str(move_speed) + ':' + str(move_time) + '&DE\n'
        print('Sending command to rover: ' + msg)
        gcs.write(msg.encode())
        
    def btnSendTurnCmd(self):
        global turn_dir, turn_time, turn_speed
        msg = '&RoverCommand&CST&CE&DS' + str(turn_dir) + ';' + str(turn_speed) + ':' + str(turn_time) + '&DE\n'
        print('Sending command to rover: ' + msg)
        gcs.write(msg.encode())
        
        
        
        
        

        
class SensorWindow(QWidget):
    
    global sb1_temp, sb1_time, sb1_heatindex, sb1_humidity, sb1_pressure, sb1_battery
    
    def __init__(self):       
        super().__init__()
        self.title = 'Sensor Data'
        self.left = 200
        self.top = 200
        self.width = 1920
        self.height = 1080
        self.qTimer = QTimer()
        self.qTimer.setInterval(50)    
        self.qTimer.start()
        self.qTimer.timeout.connect(self.update_plot_data)
        self.setWindowIcon(QtGui.QIcon('./Orbweaver.png'))
        self.initUI()
        
    def initUI(self):
        self.setWindowTitle(self.title)
        self.setGeometry(self.left, self.top, self.width, self.height) 
        self.setAutoFillBackground(True)
        p = self.palette()
        p.setColor(self.backgroundRole(), QColor(25, 35, 40))
        self.setPalette(p)
        
        #
        # Graph widgets for Sensor Box 1
        #
        
        # Temperature and Heat Index
        self.label_sb1_temperature = QLabel(self)
        self.label_sb1_temperature.setText("Temperature & Heat Index")
        self.label_sb1_temperature.setGeometry(20, 17, 420, 20)
        self.label_sb1_temperature.setFont(QFont("Arial", 18))
        self.label_sb1_temperature.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        self.label_sb1_temperature.setAlignment(QtCore.Qt.AlignCenter)
        
        self.graphWidget_sb1_temperature = pg.PlotWidget(self)
        self.graphWidget_sb1_temperature.setGeometry(20, 50, 440, 350)
        self.graphWidget_sb1_temperature.setBackground(None)
        self.graphWidget_sb1_temperature.setYRange(-20, 15, padding=0.04)
        
        data1_pen = pg.mkPen(color=(255, 7, 58), width=3)
        self.dataLine_sb1_temperature = self.graphWidget_sb1_temperature.plot(sb1_time, sb1_temp, pen = data1_pen)
        
        data2_pen = pg.mkPen(color=(255, 95, 31), width=3)
        self.dataLine_sb1_heatindex = self.graphWidget_sb1_temperature.plot(sb1_time, sb1_heatindex, pen = data2_pen)
        
        # Humidity
        self.label_sb1_humidity = QLabel(self)
        self.label_sb1_humidity.setText("Humidity")
        self.label_sb1_humidity.setGeometry(490, 17, 440, 20)
        self.label_sb1_humidity.setFont(QFont("Arial", 18))
        self.label_sb1_humidity.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        self.label_sb1_humidity.setAlignment(QtCore.Qt.AlignCenter)
        
        self.graphWidget_sb1_humidity = pg.PlotWidget(self)
        self.graphWidget_sb1_humidity.setGeometry(490, 50, 440, 350)
        self.graphWidget_sb1_humidity.setBackground(None)
        self.graphWidget_sb1_humidity.setYRange(0, 100, padding=0.04)
        
        data1_pen = pg.mkPen(color=(255, 237, 39), width=3)
        self.dataLine_sb1_humidity = self.graphWidget_sb1_humidity.plot(sb1_time, sb1_humidity, pen = data1_pen)
        
        # Air Pressure
        self.label_sb1_pressure = QLabel(self)
        self.label_sb1_pressure.setText("Air Pressure")
        self.label_sb1_pressure.setGeometry(20, 412, 420, 20)
        self.label_sb1_pressure.setFont(QFont("Arial", 18))
        self.label_sb1_pressure.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        self.label_sb1_pressure.setAlignment(QtCore.Qt.AlignCenter)
        
        self.graphWidget_sb1_pressure = pg.PlotWidget(self)
        self.graphWidget_sb1_pressure.setGeometry(20, 443, 440, 350)
        self.graphWidget_sb1_pressure.setBackground(None)
        self.graphWidget_sb1_pressure.setYRange(900, 1100, padding=0.04)
        
        data1_pen = pg.mkPen(color=(0, 200, 255), width=3)
        self.dataLine_sb1_pressure = self.graphWidget_sb1_pressure.plot(sb1_time, sb1_pressure, pen = data1_pen)
        
        # Battery Voltage
        self.label_sb1_battery_level = QLabel(self)
        self.label_sb1_battery_level.setText("Battery Voltage")
        self.label_sb1_battery_level.setGeometry(490, 412, 440, 20)
        self.label_sb1_battery_level.setFont(QFont("Arial", 18))
        self.label_sb1_battery_level.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        self.label_sb1_battery_level.setAlignment(QtCore.Qt.AlignCenter)
        
        self.graphWidget_sb1_battery_level = pg.PlotWidget(self)
        self.graphWidget_sb1_battery_level.setGeometry(490, 443, 440, 350)
        self.graphWidget_sb1_battery_level.setBackground(None)
        self.graphWidget_sb1_battery_level.setYRange(2.4, 2.9, padding=0.04)
        
        data1_pen = pg.mkPen(color=(57, 255, 20), width=3)
        self.dataLine_sb1_battery = self.graphWidget_sb1_battery_level.plot(sb1_time, sb1_battery, pen = data1_pen)
        
        #
        # Graph widgets for Sensor Box 2
        #
        
        # Temperature and Heat Index
        self.label_sb2_temperature = QLabel(self)
        self.label_sb2_temperature.setText("Temperature & Heat Index")
        self.label_sb2_temperature.setGeometry(980, 17, 440, 20)
        self.label_sb2_temperature.setFont(QFont("Arial", 18))
        self.label_sb2_temperature.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        self.label_sb2_temperature.setAlignment(QtCore.Qt.AlignCenter)
        
        self.graphWidget_sb2_temperature = pg.PlotWidget(self)
        self.graphWidget_sb2_temperature.setGeometry(980, 50, 440, 350)
        self.graphWidget_sb2_temperature.setBackground(None)
        self.graphWidget_sb2_temperature.setYRange(-20, 40, padding=0.04)
        
        data1_pen = pg.mkPen(color=(255, 7, 58), width=3)
        self.dataLine_sb2_temperature = self.graphWidget_sb2_temperature.plot(sb2_time, sb2_temp, pen = data1_pen)
        
        data2_pen = pg.mkPen(color=(255, 95, 31), width=3)
        self.dataLine_sb2_heatindex = self.graphWidget_sb2_temperature.plot(sb2_time, sb2_heatindex, pen = data2_pen)
        
        
        # Humidity
        self.label_sb2_humidity = QLabel(self)
        self.label_sb2_humidity.setText("Humidity")
        self.label_sb2_humidity.setGeometry(1450, 17, 440, 20)
        self.label_sb2_humidity.setFont(QFont("Arial", 18))
        self.label_sb2_humidity.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        self.label_sb2_humidity.setAlignment(QtCore.Qt.AlignCenter)
        
        self.graphWidget_sb2_humidity = pg.PlotWidget(self)
        self.graphWidget_sb2_humidity.setGeometry(1450, 50, 440, 350)
        self.graphWidget_sb2_humidity.setBackground(None)
        self.graphWidget_sb2_humidity.setYRange(0, 100, padding=0.04)
        
        data1_pen = pg.mkPen(color=(255, 237, 39), width=3)
        self.dataLine_sb2_humidity = self.graphWidget_sb2_humidity.plot(sb2_time, sb2_humidity, pen = data1_pen)
        
        
        # Air Pressure
        self.label_sb2_pressure = QLabel(self)
        self.label_sb2_pressure.setText("Air Pressure")
        self.label_sb2_pressure.setGeometry(970, 412, 440, 20)
        self.label_sb2_pressure.setFont(QFont("Arial", 18))
        self.label_sb2_pressure.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        self.label_sb2_pressure.setAlignment(QtCore.Qt.AlignCenter)
        
        self.graphWidget_sb2_pressure = pg.PlotWidget(self)
        self.graphWidget_sb2_pressure.setGeometry(970, 443, 440, 350)
        self.graphWidget_sb2_pressure.setBackground(None)
        self.graphWidget_sb2_pressure.setYRange(900, 1100, padding=0.04)
        
        data1_pen = pg.mkPen(color=(0, 200, 255), width=3)
        self.dataLine_sb2_pressure = self.graphWidget_sb2_pressure.plot(sb2_time, sb2_pressure, pen = data1_pen)
        
        
        # Battery Voltage
        self.label_sb2_battery_level = QLabel(self)
        self.label_sb2_battery_level.setText("Battery Voltage")
        self.label_sb2_battery_level.setGeometry(1450, 412, 440, 20)
        self.label_sb2_battery_level.setFont(QFont("Arial", 18))
        self.label_sb2_battery_level.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        self.label_sb2_battery_level.setAlignment(QtCore.Qt.AlignCenter)
        
        self.graphWidget_sb2_battery_level = pg.PlotWidget(self)
        self.graphWidget_sb2_battery_level.setGeometry(1450, 443, 440, 350)
        self.graphWidget_sb2_battery_level.setBackground(None)
        self.graphWidget_sb2_battery_level.setYRange(2.4, 3.1, padding=0.04)
        
        data1_pen = pg.mkPen(color=(57, 255, 20), width=3)
        self.dataLine_sb2_battery = self.graphWidget_sb2_battery_level.plot(sb2_time, sb2_battery, pen = data1_pen)
        
        self.show()
        
    def update_plot_data(self):
        global sb1_temp, sb1_time, sb1_heatindex, sb1_humidity, sb1_pressure, sb1_battery, batch_size
        if len(sb1_time) > batch_size:
            self.dataLine_sb1_temperature.setData(sb1_time[len(sb1_time) - batch_size:], sb1_temp[len(sb1_time) - batch_size:])
            self.dataLine_sb1_heatindex.setData(sb1_time[len(sb1_time) - batch_size:], sb1_heatindex[len(sb1_time) - batch_size:])
            self.dataLine_sb1_humidity.setData(sb1_time[len(sb1_time) - batch_size:], sb1_humidity[len(sb1_time) - batch_size:])
            self.dataLine_sb1_pressure.setData(sb1_time[len(sb1_time) - batch_size:], sb1_pressure[len(sb1_time) - batch_size:])
            self.dataLine_sb1_battery.setData(sb1_time[len(sb1_time) - batch_size:], sb1_battery[len(sb1_time) - batch_size:])
            
            self.dataLine_sb2_temperature.setData(sb2_time[len(sb2_time) - batch_size:], sb2_temp[len(sb2_time) - batch_size:])
            self.dataLine_sb2_heatindex.setData(sb2_time[len(sb2_time) - batch_size:], sb2_heatindex[len(sb2_time) - batch_size:])
            self.dataLine_sb2_humidity.setData(sb2_time[len(sb2_time) - batch_size:], sb2_humidity[len(sb2_time) - batch_size:])
            self.dataLine_sb2_pressure.setData(sb2_time[len(sb2_time) - batch_size:], sb2_pressure[len(sb2_time) - batch_size:])
            self.dataLine_sb2_battery.setData(sb2_time[len(sb2_time) - batch_size:], sb2_battery[len(sb2_time) - batch_size:])
            
        else:
            self.dataLine_sb1_temperature.setData(sb1_time, sb1_temp)
            self.dataLine_sb1_heatindex.setData(sb1_time, sb1_heatindex)
            self.dataLine_sb1_humidity.setData(sb1_time, sb1_humidity)
            self.dataLine_sb1_pressure.setData(sb1_time, sb1_pressure)
            self.dataLine_sb1_battery.setData(sb1_time, sb1_battery)
            
            self.dataLine_sb2_temperature.setData(sb2_time, sb2_temp)
            self.dataLine_sb2_heatindex.setData(sb2_time, sb2_heatindex)
            self.dataLine_sb2_humidity.setData(sb2_time, sb2_humidity)
            self.dataLine_sb2_pressure.setData(sb2_time, sb2_pressure)
            self.dataLine_sb2_battery.setData(sb2_time, sb2_battery)
        
    def paintEvent(self, event):
        global borderColor, borderWidth
        
        painter = QPainter()
        painter.begin(self)
        painter.setRenderHint(QPainter.Antialiasing)
        
        # Pen for drawing the sensor box borders
        painter.setPen(QPen(borderColor, borderWidth))
        
        # Main sensor box borders
        painter.drawRect(10, 10, 940, 1000)
        painter.drawRect(970, 10, 940, 1000)
        
        # Sensor box input data retangles
        painter.drawRect(10, 800, 940, 210)
        painter.drawRect(970, 800, 940, 210)
        
        # Drawing the graph separators
        painter.drawLine(475, 10, 475, 800)
        painter.drawLine(10, 405, 945, 405)
        painter.drawLine(650, 800, 650, 1010)
        painter.drawLine(1430, 10, 1430, 800)
        painter.drawLine(970, 405, 1910, 405)
        painter.drawLine(1620, 800, 1620, 1010)
        
        painter.setPen(QPen(borderColor, borderWidth / 2))
        
        painter.drawLine(10, 40, 945, 40)
        painter.drawLine(970, 40, 1910, 40)
        painter.drawLine(10, 435, 945, 435)
        painter.drawLine(970, 435, 1910, 435)
        
        
        
        

class Controller:
    
    def __init__(self):
        pass
    
    def show_rover_window(self):
        self.rover_window = RoverWindow()
        self.rover_window.show()
        
    def show_sensor_window(self):
        self.sensor_window = SensorWindow()
        self.sensor_window.show()

    
def main():
    app = QApplication(sys.argv)
    controller = Controller()
    controller.show_rover_window()
    controller.show_sensor_window()
    sys.exit(app.exec_())
    
def serialDataFunction():
    global gcs
    
    while True:
        gcs_data = str(gcs.readline())
        parseData(gcs_data)
    

# Main function
if __name__ == "__main__":
    
    serialThread = threading.Thread(target = serialDataFunction)
    serialThread.start()
    
    main()

The code has a thread for the windows and has a thread for listening to the data coming from the GCS. Here are the pictures of the GCS in action.

imageimage

5. Testing the System

With all of that work done, it's time to play a bit and test the functionality of the system. Before driving the rover around, there is one short test I wanted to conduct.

Freezer Test

While I already subjected the Sensor Box to Rain and -10C weather, I thought it would be a good idea to see how that compares to the inside of my freezer.

imageimage

I let the test run a bit longer inside the freezer and it got to about -24C before it started oscillating around -23C. The box handled it all no problem at all, I just won't be opening it right away so that the moisture from the outside doesn't get to the freezing cold components inside and condensate.

Obstacle Test

In this test, I put a small obstacle in front of the rover to see how the suspension performs. You can see the body trying to balance itself as the rover is climbing up on the obstacle and then climbing down it.

You don't have permission to edit metadata of this video.
Edit media
x
image
Upload Preview
image

While the rubber bands are a great way for saving the motors, they can start sleeping pretty easily at which point we lost all of the torque at the wheels. I'm really interested to see how this would perform with the geared gearboxes that I mentioned in one of the previous blogs.

Picking up the sensor box

In this test, I drove around the living room a bit, showcasing how the rover moves as well as picking up the sensor box and then leaving it on the floor. This type of box pick-up method can scale up to 2 boxes, but a more complex system would be needed if we wanted to use many boxes. One idea I has for a system like that was to maybe make a small trailer for the rover that would contain all of the sensor nodes.

You don't have permission to edit metadata of this video.
Edit media
x
image
Upload Preview
image

It didn't get that warm while driving around the rover. I think this will change once I add the Raspberry.

image

6. Summary

To wrap up this project, I'll go over where I think this concept could actually be useful, what went well with this project, and what went bad. I'll also share some of my initial sketches and drawing that got this whole project started. Let's begin with where I think this concept can be useful.

Real-life use cases 

In my opinion and my goal with this project was to design and make a concept of a system where we have a rover that's going around putting sensor boxes and monitoring the relevant data. The benefit of a system like this is that the rover can go to places where humans can't go or it's dangerous for them to go. Some examples would be:

  • Toxic gas leak monitoring
    • The rover could be deployed to strategically place sensor boxes to monitor how the concentration of the gas is changing throughout a wider area. This can be useful especially when we consider changing winds and other similar factors and it's important for us to react fast.
    • Using a BLDC drive system with some other conversions a system like this could be used in monitoring explosive gas leaks as well
  • Volcano eruption monitoring
    • During an eruption, a lot of poisonous gasses are released by the volcano, considering the rover's offroad capabilities, we can send it to put sensor boxes around the area
  • Making a perimeter
    • The rover can go in a line dropping sensor boxes with IMU-s and vibration sensors making an invisible fence. This can be used for tracking the movements of animals.
  • Mine/tunnel inspection
    • Dangerous areas, not the healthiest environment
    • To extend the range of the rover, we could drop a sensor box every 100m for example, and program them so we have a daisy chain style of communication

What went well

I am extremely happy with how this project turned out in the end if I'm honest. One thing that surprised me the most is how little hassle and troubleshooting I had to do when it comes to wiring because I took my time to do it slowly and label every cable. I am also happy at how rigid the whole construction it is as well as how good the suspension setup is working. LoRa turned out to be really easy to work with once we update the firmware on the MKR WAN boards. I noticed some smaller problems when there are multiple devices sending data at the same time, but using an RTC, this can easily be solved with some timing. It was also my first time doing a 2 window GUI, so I am thrilled it worked out well!

What went wrong

While most of the things went smoothly, there are some things I am not completely happy with. While 3D-printed parts can be really strong, I had to print some of them faster, so they aren't as strong. This mostly is aimed at the leg pipe holders, the pipes move a bit over time, but I get a PLA cracking noise if I try tightening the screws anymore. The biggest issue I has was with the servo motors. I first went with MG995 servos, which should be all metal gear servos. Here is what I found inside.

image

While the output gear was made out of metal, all of the gears inside were made out of plastic, which is a horrible combo. I noticed that the servos weren't working properly and here is the reason for it. I had to order 4 new MG996R servos from a different supplier and these actually were all-metal gear servos. The only servo left with both plastic and metal gears is the suspension servo. That's one of the reasons for so much free play in the system that can be seen in the videos. The only thing that saves it is that it's a continuous servo, so it gets to good teeth eventually.

Sketches

Here are some sketches I did while working on this project. While some ideas made it into the final build, I gave up on others, or still didn't have time to incorporate them into the rover, but will get to them eventually.

{gallery}Sketches

image

IMAGE TITLE: Sketch 1

image

IMAGE TITLE: Sketch 2

image

IMAGE TITLE: Sketch 3

image

IMAGE TITLE: Sketch 4

image

IMAGE TITLE: Sketch 5

image

IMAGE TITLE: Sketch 6

image

IMAGE TITLE: Sketch 7

image

IMAGE TITLE: Sketch 8

image

IMAGE TITLE: Sketch 9

image

IMAGE TITLE: Sketch 10

image

IMAGE TITLE: Sketch 11

image

IMAGE TITLE: Sketch 12

image

IMAGE TITLE: Sketch 13

image

IMAGE TITLE: Sketch 14

image

IMAGE TITLE: Sketch 15

image

IMAGE TITLE: Sketch 16

image

IMAGE TITLE: Sketch 17

image

IMAGE TITLE: Sketch 18

Closing Note

This was a really fun design challenge. I've followed other projects while working on mine and there are so many great ones! I would like to thank Element14 and Hammond Manufacturing for hosting and sponsoring this design challenge. This was one of the bigger and tougher projects I've completed so far, but it was really rewarding to see the rover moving and driving around the house in the end! While I didn't do everything I set to do, I'm still extremely happy with how the project turned out in the end. The final codes for this project are in this blog, if I make any changes to that, I'll be sure to post all of that on GitHub which is linked below. You can also find the whole rover assembly in STEP format on GitHub, and I'll upload individual parts as both STL-s and STEP files. Thanks for reading the blog, hope you enjoyed reading about my journey with the Rover!

image

Milos

Relevant links for the competition:

  • Just Encase Design Challenge
  • Just Encase Design Challenge - About
  • Just Encase Design Challenge - Challengers

Link to my GitHub where you can find all of the files used for this project (codes, 3D models,...):

  • GitHub Orbweaver Rover

Link to my Project Collection:

  • Milos Rasic - Project Collection
  • Sign in to reply
  • beacon_dave
    beacon_dave over 3 years ago

    ...This type of box pick-up method can scale up to 2 boxes, but a more complex system would be needed if we wanted to use many boxes...

    You could perhaps make use of the 4 green bogie arms for box pick-up, a bit like the thigh pockets on 'cargo pants'. Have a pick-up claw rotate 180degrees around each arm. 

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • More
    • Cancel
  • genebren
    genebren over 3 years ago

    Great Job!  I really think that you nailed so many aspects of your robotic platform.  I can see a lot of potential for this robot!

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • More
    • Cancel
  • dubbie
    dubbie over 3 years ago

    This is a really great mobile robot. I want one! A great project that looks good fun.

    Dubbie

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • More
    • Cancel
element14 Community

element14 is the first online community specifically for engineers. Connect with your peers and get expert answers to your questions.

  • Members
  • Learn
  • Technologies
  • Challenges & Projects
  • Products
  • Store
  • About Us
  • Feedback & Support
  • FAQs
  • Terms of Use
  • Privacy Policy
  • Legal and Copyright Notices
  • Sitemap
  • Cookies

An Avnet Company © 2025 Premier Farnell Limited. All Rights Reserved.

Premier Farnell Ltd, registered in England and Wales (no 00876412), registered office: Farnell House, Forge Lane, Leeds LS12 2NE.

ICP 备案号 10220084.

Follow element14

  • X
  • Facebook
  • linkedin
  • YouTube