Assignment 15

Assignments

Individual Assignments

  • Write an application that interfaces a User with an input and/or output device that you made

Group Assignments

  • Compare as many Tools as possible

Assignments

Group Assignment

The Group Assignment for this week can be found here.


Individual Assignment

This wekk I am going to do two seperate things. For one, I really wanted to showcase my work from last week and create an application that receives the data and displays the ISS on a two dimensional map of the earth. Since this application would not fulfill the complete assignment (interfacing a User), I also went ahead and put together an application that shows the sensor readings of an ultra sonic distance rmeasuring sensor. In order to start with the task that satisfies the assignment, I will begin with the Ultra Sonic Sensor.

For this assignment, I was really excited about the local lecture where we got to learn Tkinter and so inherently inteface programming with python. I had programmed GUIs and interfaces before, like within Unity or with C# or in the earlier days of my studies with Java and swing and later on JavaFX. But since python is quite different to all of these languages, I was very excited to learn more about Tkinter. We learned all the basics and build some simple sample GUIs and then were left off to do our own thing! Since I already had the ISS thing in mind, I wanted to just make a cool GUI that interfaces the User with one of the sensors I had with me. For simplicity I chose the "HC-SR04" Sonar distance sensor. After having a look a look at the example code within Arduions IDE, I gathered the code I needed for gathering the distance measured and just send it out via a serial signal with a given Baud rate. The Arduino code using my two Cat Boards (one for interfacing with the sensor, the other one for interfacing the first Board with my Laptop), looks like this:

					
#define trigger 3 //Triggerpin on my cat Board
#define echo    9 //Echo pin on my cat Board

unsigned long duration; //Variable to store duration between echo being send out by TRIGGER and then received by ECHO
unsigned int distance; //Variable to store calculated distance

void setup()
{
  Serial.begin(9600); //Start Serial with 9600 Baud
  pinMode(trigger, OUTPUT); //Set trigger pin to output
  pinMode(echo, INPUT); //set echo pin to input
}

void loop()
{
  digitalWrite(trigger, LOW); //reset trigger pin
  delayMicroseconds(2);

  digitalWrite(trigger, HIGH); //send trigger signal
  delayMicroseconds(10);

  duration = pulseIn(echo, HIGH); //check for received trigger signal and save time since trigger sent
  distance = duration/58; //Divide by 58 to get distance (often used value when disregarding temperature while using speed of sound)

  Serial.println(distance); //print distance over serial
 
  delay(250); //Wait to not overcrowd the signal with itself
}
					
				

With this, the sensor side was done. Now onto the application side. I wanted to allow the User to scan for connected serial ports. The User could then select the Serial port and open it, which would then open a second Window, where the sensor data is being shown in some kind of way. To achieve this, I started with the main window, where the User could choose the serial port he or she wants to use. For this I needed a Combobox and two buttons. The combobox would store all the ports found. The first button would scan for ports and the second button would connect to the port currently selected with the combobox. Getting all of the elements inside the window was very easy. A little more complucated wads the process of filling the combobox with the found ports. For windows users this easy. The ports are just called COMX, where X is some number between 1 and 256. When I first did this with Windows everything wads fine. pyserial has a function to scan for serial ports. I just usee what I needed from the resulting list and stuffed it inside of the combobox. When connectiong to the port I would just pull the string currently selected in the combobox and open an serial connection with this string as the port name. Like I said, for Windows, this worked beautifully. When I tried out the program on MacOS however, it did not work at all. MacOS handles Serial ports very much differently than windows. Instead of having 256 ports that can be used, MacOS sort of virtualizes the ports. On top of that each device and or connection is being given a string to identify this port. When selecting a port on MacOS the ports are built like follows: /dev/cu.usbserial-XXXXXXX. The problem I had was that pyserial function for getting all available serial ports would only give me back everything after /dev/. To combat this is just added the missing part infront of the string and it worked. The problem is however, that like this, the program is not usable across al devices. I would need some kind of function that gathers OS information and handles the ports found by pyserial accordingly. Since this would however be a little bit over the top, I just stuck with my solution of only using MacOS or Windows for now. The next big problem I had was figuring out, how to generate a second child window. In the end it was much simpler than I anticipated. I just had to use the TopLevel function with my root window as a parameter. With this done I designed the second window just like all the designs we saw for sensors in the lecture. After this was done, I just had to setup a loop with the after() function to gather the newest sensor readings over and over again. In the end the whole process loooked like this:

Selecting a serial port and reading ultrasonic sensor data!

The code for the application is as follows:

					
from tkinter import *
from tkinter.ttk import Combobox
import serial
import serial.tools.list_ports

port = ''
portlist = []
readData = ''
window = 400
portWindow = None

ser = None

root = Tk()
#root.title=("sensorReadOutTest")
root.geometry("200x150")
root.resizable(False, False)

def refreshPorts():
    global portlist
    global selectionBox
    portlist.clear()
    ports = serial.tools.list_ports.comports()
    for port in sorted(ports):
        portlist.append((port.name, port.description))
    print(portlist)
    selectionBox.configure(values=[port[0] for port in portlist])

def openPort():
    global portWindow
    global port
    global ser
    port = "/dev/" + selectionBox.get()
    ser = serial.Serial(port, 115200)
    print("Opened port: " + port)
    portWindow = Toplevel(root)
    portWindow.title("Sensor Reading")
    portWindow.geometry("400x200")
    portWindow.resizable(False, False)
    text = "new Window on Port: " + port
    canvas = Canvas(portWindow, width=window, height=.5 * window, bg="black")
    canvas.create_rectangle(0, 0, window, .5 * window, fill="black", tags="bg_rect")
    canvas.create_rectangle(0, 0, .5*window, .5 * window, fill="green", tags="fr_rect")
    canvas.create_text(50, .25 * window, text='Value', font=("Helvetica", 24), tags='text', fill="white")
    canvas.pack()

    #start idle loop
    ignore = True
    portWindow.after_idle(idle, portWindow, canvas, ser, ignore)


def idle(parent, canvas, ser, ignore):
    ser.flush()
    line = ser.readline()
    print(line)
    if not ignore:
        line = line.decode("utf-8")
        if int(line) and not line.isspace():
            if (int(line) > 400):
                newX = 400
            else:
                newX = int(line)
            canvas.coords('fr_rect', 0, 0, 400 - newX, .5 * window)
            canvas.itemconfigure("text", text=str(newX))
            canvas.update()
    else:
        ignore = False
    parent.after(100, idle, parent, canvas, ser, ignore)

def closePort():
    global ser
    ser.flush()
    ser.close()
    print("closed Port: " + port)

selectionBox = Combobox(root, values=[])
selectionBox.set("Choose Serial Port")
selectionBox.pack(pady=10)

myButton = Button(root, text="Rescan for Ports", command=refreshPorts)
myButton.pack(pady=10)

startButton = Button(root, text="Open Serial Port", command=openPort)
startButton.pack(pady=10)

refreshPorts()

root.mainloop()
					
				

With this done, I wanted to finally visualize my ISS positional data! Since there is no human interaction here, this is more or less just a bonus, but I did not want to let the work last week go to nothing! For this I made to different classes. One for handling the window, and the other one for handling the dataflow through the API.

Below is the Code I used with my ESP connected like shown in Networks and Communications week.

Main Class:

					
from tkinter import *   #import everything from tkinter
from PIL import ImageTk, Image #import Pillow for handling images
import PositionTracker #import PositionTracker (My other Class)
import serial #impoer pyserial

x = 0 #define x
y = 0 #define y

ser = serial.Serial('COM12', 115200) #Open Serial port for data by ESP

root = Tk()  # Generate root window
root.title("ISS Tracker")  # Give window title "ISS Tracker"
root.geometry("1500x750")  # Force window size to 1500x750
root.resizable(0, 0)  # Force window to be non resizable
root.iconphoto(False, ImageTk.PhotoImage(Image.open("ISS.png").resize((50, 50))))  # Change taskbar icon of GUI
img = ImageTk.PhotoImage(Image.open("WorldMap.jpg").resize((1500, 750)))  # Load up World map resized to 1500x750
mapCanvas = Canvas(root, width=1500, height=750, bg="white")  # Generate 1500x750 canvas
mapCanvas.pack(pady=20) # pack canvas
bgImage = mapCanvas.create_image(0, 0, anchor=NW, image=img)  # Load world map as background image for canvas
iss = ImageTk.PhotoImage(Image.open("ISS.png").resize((50, 50)))  # Load ISS image
x = int(750)  # Set initial Coordinate to middle of screen
y = int(375)  # Set initial Coordinate to middle of screen
issImage = mapCanvas.create_image(x, y, image=iss, tags='ISS')  # Set ISS image onto world map canvas at (x,y)


def updatePosition():  # function to update position
	line = ser.readline()  # read one line from serial connection
	line = line.decode('utf-8').strip()  # decode bytearray to utf-8 and strip away "\n" from the end
	ser.flush()  # Flush the connection
	if line and not line.isspace():  # Check if line isn't empty and if line is not just whitespace
		positions = PositionTracker.gatherPosition(line)  # Call positiontracker with information gotten via serial, save information into positions
		mapCanvas.delete('ISS')  # Delete current ISS image
		issImage = mapCanvas.create_image(int(positions[0]), int(positions[1]), image=iss)  # Create new ISS image at newly calculated position
		mapCanvas.create_text(225, 15, font="Times 20 italic bold",
		text='latitude: ' + positions[2] + ' | longitude: ' + positions[3])  # Create text to show Lat and Lon

	root.after(1500, updatePosition) # Recall this function after 1500ms


updatePosition()  # First call of UpdatePosition
root.mainloop()  # main loop of root window
						
					
				

PositionTracker Class:

					
import urllib3  # import urllib3 to handle http request (When using the API directly)
import json  # Decode JSON (When using the API driectly)
import math  # import math for calculating the (x,y) from lat and lon

http = urllib3.PoolManager()  # generate pool manager to later make http requests (When using the API driectly)
response = None  # Define response object that later gets filled (When using the API driectly)


latitude = None  # Define latitude
longitude = None  # Define longitude
mapWidth = 1500  # Define mapWidth
mapHeight = 750  # Define MapHeight

def gatherPosition(line):  # Strip the ESP String into two floats
	position = line
	print("pos: " + position)
	lat = position[0:position.index(',')]
	lon = position[position.index(',') + 1:len(position)]
	latitude = lat[lat.find('"') + 1:lat.rfind('"')]
	longitude = lon[lon.find('"') + 1:lon.rfind('"')]
	calcPos = convertLatLon(latitude=latitude, longitude=longitude, mapWidth=mapWidth, mapHeight=mapHeight)
	return calcPos  # Return Calculated Values


def convertLatLon(latitude, longitude, mapWidth, mapHeight):  # Convert from Latitude and longitude to (x,y) in given sizes
	falseEasting = 180
	radius = mapWidth / (2 * math.pi)

	latRadian = math.radians(float(latitude))
	lonRadian = math.radians(float(longitude) + falseEasting)

	x = lonRadian * radius

	yFromEquatorradius = radius * math.log(math.tan(math.pi / 4 + latRadian / 2))
	y = mapHeight / 2 - yFromEquatorradius

	return [x, y, latitude, longitude]  # return (x,y) and (Lat,Lon)
					
				

When you want to use the API directly, you only need to change the gatherPosition function of the PositionTracker Class. Instead of receiving the string from the main class via Serial, a HTTP GET request is being generated. The result of this request is saved, decoded from JSON and then used further on! Obviously the serial connection also has to be cut away in the main class. After testing everything, I let the program run for a few hours, resulting in this timelapse: