Orbweaver Rover
Orb-weaver Rover - Making the Rover! <----- Previous Blog
Table of Contents
1. 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:
- Show the changes I made to the rover compared to the previous blog
- Go through all of the blogs and what I've accomplished in them
- Show off the final setup of the whole system
- Talk about what went great and what went wrong
- 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.
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.
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.
That would cover the major hardware changes compared to the last blog. Here is how they look in real life.
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.
- 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!!!
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.
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.
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.
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.
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.
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.
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!
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:
- Code for the Sensor Boxes
- GCS code
- Rover Arduino Mega Code
- Rover Arduino MKR WAN code
- 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.
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.
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.
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.
It didn't get that warm while driving around the rover. I think this will change once I add the Raspberry.
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.
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 TITLE: Sketch 1 |
IMAGE TITLE: Sketch 2 |
IMAGE TITLE: Sketch 3 |
IMAGE TITLE: Sketch 4 |
IMAGE TITLE: Sketch 5 |
IMAGE TITLE: Sketch 6 |
IMAGE TITLE: Sketch 7 |
IMAGE TITLE: Sketch 8 |
IMAGE TITLE: Sketch 9 |
IMAGE TITLE: Sketch 10 |
IMAGE TITLE: Sketch 11 |
IMAGE TITLE: Sketch 12 |
IMAGE TITLE: Sketch 13 |
IMAGE TITLE: Sketch 14 |
IMAGE TITLE: Sketch 15 |
IMAGE TITLE: Sketch 16 |
IMAGE TITLE: Sketch 17 |
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!
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,...):
Link to my Project Collection: