Due to the limited wifi access on the day, I want to make my code as easy to interface as possible without the need for an external source (such as a laptop). With this in mind, I decided to purchase a Pimoroni GFX Hat to try and make a simple and easy to use menu to interface with my robot. Although the screen has to be functional, I also wanted to make sure that it looked good. Because of this, I wanted to make icons that could be used to identify each challenge, as well as some simple commands, such as shutdown, reboot and access settings.
Icons
Because of the size of the screen, I limited my icons to being around 128×40 pixels to enable me to include a status bar at the top, and relevant text along the bottom. I started off by making image files, and using the Python PIL library to convert them to black and white pixels to be used on the screen. I soon found this to be a very slow process, as the user could rapidly press the next button, but the code would still be struggling to load an image, causing a large deal of lag. After a little bit of debugging, I soon found the speed issue to be the conversion of the image from a PNG to black and white image object in PIL. To resolve this, I decided to pre-define the pixels in each image in a series of icon functions. These an then be stored in a different module, and be called from the main menu program when required. I started off by making a black and white PNG image, and then used a small python script to generate the coordinates required. These can then be appended to an array of coordinates, which can then be drawn to the screen. The program saved me hours of time which I would have had to manually go through each image getting the coordinates of each black pixel.
from PIL import Image im = Image.open("settings.png") # Image in question pix = im.load() # Loading image size = str(im.size) # Getting size of image size = size.replace("(", "") # Manipulating size do can be numeric size = size.replace(")", "") # Manipulating size do can be numeric size = size.replace(" ", "") # Manipulating size do can be numeric xmax, ymax = size.split(",") # setting x and y max of image SettingsFile = open("export.txt", "w") # Creating file to add appends to to be copied into icon program for x in range(int(xmax)): # Loop through each x coordinate of image for y in range(int(ymax)): # loop through each y coordinate of x row if str(pix[x,y]) == "(0, 0, 0, 255)" or str(pix[x,y]) == "(0, 0, 0)": # Check if pixel is black SettingsFile.write("cords.append((" + str(x) + "," + str(y) + "))\n") # Add the array append to the temp file SettingsFile.close() # Close the written file print("Done")
I did end up spending a little time on some of the icons, with my favourites below being the Spirit of Curiosity, Hubble Telescope Nebula Challenge & blastoff
Building A Setting Class
One thing I wanted the ability to do was to be able to load various settings between boots. To do this, I created a text file and settings class which can be loaded and saved within the program. I am planning to re-visit this in the future, as I feel there is possibly a better way to do it, however for the time being it seems to do the job. Initially, I created the base class which loads the settings from a text file. An example text file looks like:
255 255 255 [255, 89, 136] [36, 169, 255] [198, 255, 142] [42, 205, 255]
To load this I created the class with the below code. It loads the lines of the text file and sets the values to the appropriate property of the class:
class Settings: def __init__(self): SettingsFile = open("Settings.txt", "r") # Read file contents to variable r = str(SettingsFile.readline()) # reading through each line to set appropriate value g = str(SettingsFile.readline()) b = str(SettingsFile.readline()) nebRed = str(SettingsFile.readline()) nebBlu = str(SettingsFile.readline()) nebYel = str(SettingsFile.readline()) nebGre = str(SettingsFile.readline()) self.blR = r.replace('\n', '') # clean values to make numeric, removing new lines self.blG = g.replace('\n', '') self.blB = b.replace('\n', '') self.nebRed = nebRed.replace('\n', '') self.nebBlue = nebBlu.replace('\n', '') self.nebYellow = nebYel.replace('\n', '') self.nebGreen = nebGre.replace('\n', '')
Once I then added a save file function to the class, so that the settings could be saved on the fly. This should then allow any values saved to to the settings class to be reloaded on the next boot.
def saveSettings(self): SettingsFile = open("Settings.txt", "w") # Open file to write SettingsFile.write((str(self.blR) + '\n' + str(self.blG) + '\n' + str(self.blB) + '\n' + str(self.nebRed) + '\n' + str(self.nebBlue) + '\n' + str(self.nebYellow) + '\n' + str(self.nebGreen))) # Save values with new lines to seperate SettingsFile.close() # Close file with new lines written
Display Location
I have mounted the pi on a stand on the upper chassis to make it easily accessible on the day, this does however mean that the display is upside down. To handle this, as part of the process of displaying the image to the screen, I have added an additional process to rotate the image 180 °. This means there is very limited impact to code and if it was needed to be changed, I would only need to alter the code in one place.
def gfxDisplay(image): image = mdlPil.rotateImage(image, 180) # Rotate the image 180° for x in range(128): # Go through each x row for y in range(64): # go through each y row for each x row pixel = image.getpixel((x, y)) # Identify whether pixel needs to be on or off lcd.set_pixel(x, y, pixel) # Set GFX pixel to value lcd.show() # Update display
Status Monitoring On The Go
In addition to accessing programs and features, it is also useful to be ale to see the status of both the controller and motor controllers to see if they are connected. This should help with on the go trouble shooting on the day. Initially, I want to see if the controller is connected. To do this, I can query the Pi’s Bluetooth controller to see if the mac address is connected. Depending on the status, I can then show a pre-defined icon, to show in the upper segment of the display:
def ControllerCheck(image): stdoutdata = sp.getoutput("hcitool con") # Check for connected devices if "00:06:F7:13:66:8F" in stdoutdata.split(): # Checking if PS3 mac address is in connected devices image = mdlPil.Controller(True, image) # Show connected icon else: image = mdlPil.Controller(False, image) # Show disconnected icon return image # Return the status bar for the menu
To check if the thunderborg’s are connected, I use their library to query each board to see if the controller has been found. This is then passed to the icon library which returns an image to the status bar. This can be seen below:
def TBCheck(image): TB1 = ThunderBorg.ThunderBorg() # Loading class TB1.i2cAddress = 10 # Setting address TB1.Init() # Initialising chip image = mdlPil.TB(TB1.foundChip, image, 1) # Passing if chip found to icon module TB2 = ThunderBorg.ThunderBorg() # Loading class TB2.i2cAddress = 11 # Setting address TB2.Init() # Initialising chip image = mdlPil.TB(TB1.foundChip, image, 2) # Passing if chip found to icon module return image # Retuning status bar to menu
Once all the items are put together, it enable a menu as seen below, with the status bar at the top, and icon in the middle:
Whats left to do?
To finish off, I need to work on a way to easily execute the challenge programs, but still be able to stop them using the menu. I think the best way to do this will be using threading, loading the challenge programs onto a thread, killing the thread when needed.
Overall I feel I have created both a easy to read but also visually appealing menu, using the GFX to its full capabilities. I hope that it shall prove very useful during the competition!