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:
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: