© Liz Clark 2019
Liz ClarkPractical Tinker Boardhttps://doi.org/10.1007/978-1-4842-3826-4_10

10. Project 4: Using an e-Paper Display for Weather Data

Liz Clark1 
(1)
Boston, MA, USA
 

After going over some specialized distributions of Linux for the Tinker Board, it’s time to return to where this book began: TinkerOS. This time around, we’re going to be applying the Linux, GPIO, and programming skills that we gained in the first part of the book. The first project that we’ll tackle is an Internet-connected display with an e-Paper module that will show the current date, time, and weather.

What Is e-Paper?

Often referred to as e-Ink or EPD , e-Paper (electronic paper) is a digital display that mimics the look of ink on paper. It’s the same type of display found in e-readers and other similar devices. It has a slow refresh rate, so it’s best for static displays, such as images or slowly changing text. Because of this, it has incredibly low power consumption. e-Paper is also known for being easily visible in all lighting conditions, including direct sunlight, and it’s easier on your eyes because it doesn’t emit the blue light found in LCD displays.

e-Paper comes in a variety of shapes and sizes. Traditionally, the displays are dual-color; usually black and white, but newer versions can showcase other colors, and tricolor displays are also beginning to come to market. They’re becoming increasingly popular for DIY applications, since they’re available for multiple platforms. Many displays can be coded in multiple coding languages with manufacturer-provided libraries that you can install, along with example code to test included. They usually connect through individual broken-out pins to attach to GPIO pins on different boards, but there are also specialized add-on boards that fit directly onto certain boards’ GPIO layouts, including the Tinker Board.

The display that we’re going to use for this project is from Waveshare , a manufacturer of DIY electronics accessories. They have quite a few varieties of e-Paper displays available, and the one we’ll be looking at is the 2.13-inch HAT variety. It was originally designed to work with Raspberry Pi, but ASUS has ported the Python libraries to work with the Tinker Board so that no code adjustment is needed. We’ll examine what they did, though, so that you can try porting hardware libraries for future projects.

SPI

You may be wondering how the e-Paper displays work. Some, including the one we’ll be using, use Serial Peripheral Interface (SPI) communication . SPI is a bus protocol used to communicate between devices. It has a built-in clock signal that is sent in conjunction with any data for highly accurate and timely communication. SPI is integrated into Python using the spidev library, which we’ll discuss shortly as we prepare for the Waveshare library installation.

Early Issues with SPI on the Tinker Board

Previously, there were some issues with SPI on the Tinker Board. Although both the original Tinker Board and Tinker Board S have two SPI controllers, SPI0 and SPI2, available via the GPIO, only SPI2 was enabled at the kernel level. Beginning with TinkerOS 2.0.7, this issue has been resolved and now both SPI0 and SPI2 can be utilized. This increases compatibility with a lot of hardware.

To ensure that your chosen SPI controller is enabled, navigate to a terminal in TinkerOS and enter sudo nano /boot/hw_intf.conf to edit the text file for I2C and SPI communication settings. Once in the nano file, simply change the setting next to the different functions from Off to On by typing, as shown in Figure 10-1, and then save the file. You’ll need to reboot to finish enabling SPI or other protocols. It’s important to note that some GPIO pins control multiple functions; and by enabling functions that share pins, you may run into some issues. To play it safe, only enable the ones that you need at the time and always refer to the pinout to double-check.
../images/465130_1_En_10_Chapter/465130_1_En_10_Fig1_HTML.jpg
Figure 10-1

The nano text file for I2C and SPI settings

The e-Paper Display’s Hardware

The e-Paper display that we will use has two major parts: the actual display and the PCB HAT. The display has a short ribbon cable that plugs into the side of the HAT, as shown in Figure 10-2. The HAT then breaks out the signals from the ribbon cable to the components on the board and eventually the GPIO pins.
../images/465130_1_En_10_Chapter/465130_1_En_10_Fig2_HTML.jpg
Figure 10-2

The EPD’s ribbon cable that connects to the HAT

This HAT also has a separate breakout connector to connect the individual signals in lieu of using the 40-pin GPIO header that you can also observe in Figure 10-2. You’d use this with other types of development boards or if you didn’t want the display to sit directly on top of the Tinker Board, depending on your project’s housing. Coincidentally, the 40-pin header is a bit short for this display and can’t clear the stock heat sink for the Tinker Board. You can use a 40-pin header to act as a riser so that the HAT can comfortably fit on the Tinker Board with the heat sink as shown in Figure 10-3.
../images/465130_1_En_10_Chapter/465130_1_En_10_Fig3_HTML.jpg
Figure 10-3

The 40-pin header being used as a riser for the 2.13-inch EPD HAT

Setting Up the EPD’s Software

As mentioned previously, we’re going to use the EPD with TinkerOS. Boot into TinkerOS and then navigate to the terminal so that we can begin installing the Python dependencies.

First, make sure you’re in the Home directory by changing directories with cd ~, and then run sudo apt-get update followed by
sudo apt-get install python-dev python-setuptools python-pip python-wheel python-pil

pip is a package management system that allows you to install Python dependencies, tools and libraries for Python, similar to using apt-get install or git for other programs and downloads in Linux. However, best results for installation occur when you’re targeting the Home directory. Using pip, we’re going to install the SPI library by entering pip install spidev into the terminal. After that, run sudo apt-get upgrade and sudo reboot.

Note

Some dependencies, like python-dev , were installed during other chapters. They’re repeated here in case you didn’t follow directly along with those chapters. Trying to install them again won’t damage your build of TinkerOS; they’ll just be ignored.

Next, we’ll install the libraries for this display. As discussed, Waveshare has many different e-Paper displays, and they all have different libraries, meaning that one display’s library will not work with another. Specifically, for our display, you’ll remember that there is a ported version of the library so that it will work with the Tinker Board, which was developed by a member of the ASUS team. This ported version is currently hosted on the Tinker Board’s wiki site in a 7zip folder,1 as shown in Figure 10-4.
../images/465130_1_En_10_Chapter/465130_1_En_10_Fig4_HTML.jpg
Figure 10-4

The 7zip file hosted on the Tinker Board wiki site for Waveshare add-ons. It’s located at the very bottom of the web page, which is linked in Footnote 1.

You can download and unzip the folder either on your main computer or on the Tinker Board. If you access it with the Tinker Board, best results for unzipping were experienced using the GUI tools rather than the terminal since it’s a 7zip folder rather than a zip folder. Once you open the folder, you’ll see files labeled epd2in13.py, epdif.py, main.py, and monocolor.bmp. The epd2in13.py and epdif.py files are the library files. You’ll need to have these files in the same folder as your Python program for the display so that it runs properly. The main.py file is a demo program for the display, and monocolor.bmp is a bitmap picture formatted to appear properly on the display. The main.py program has some lines of code that call for this bitmap to be displayed.

Ported Libraries

Before we see the final result of these ported libraries, let’s see what makes them tick. Studying these will also help in understanding how to get started with porting code and libraries to the Tinker Board later.

First, let’s look at epdif.py . We’ll do that by first changing directories to the newly unzipped folder and then entering idle epdif.py into the terminal. This opens any file with IDLE. Looking at the beginning of the file, as shown in Figure 10-5, we see some libraries being imported; the most notable is import ASUS.GPIO as GPIO. Previously this would have been calling for the import of the Raspberry Pi GPIO library. When adjusting code or libraries for the Tinker Board, this will be one of the easiest and most important changes to make.
../images/465130_1_En_10_Chapter/465130_1_En_10_Fig5_HTML.jpg
Figure 10-5

The epdif.py file. You can reference this figure for the entire discussion of the epdif.py file.

Looking further, we can also see that the spidev library is being imported here, which implies that SPI settings along with GPIO settings will be taken care of with this file. Sure enough, the next section of code defines the pins needed from the GPIO for SPI. Pins for Reset, DC (Data/Command control), CS (SPI chip select), and Busy (Busy state output) for the e-Paper display are assigned to the proper Tinker Board physical board pins. This is another item you’ll want to check, since many Raspberry Pi libraries utilize a different numbering convention.

Next, we see SPI being set up to run with the SPI2 bus by calling spidev.SpiDev(2, 0). If you wanted to change this to run on SPI0, you’d call spidev.SpiDev(0, 0).

Following SPI, we have some digital_write and digital_read functions for the GPIO pins that were previously defined. But the most notable and important function is epd.init() , which will be called for most major commands with the e-Paper display. It basically finishes setting up the GPIO pins and then resets them to their default states. You can see the call for the GPIO.Board numbering convention, followed by code that sets Reset, DC, and CS as outputs and Busy as an input. You can also see that the SPI speed and mode are defined as well. Later, when you see epd.init() called, know that this process is happening along with the other parameter that will be inside the parentheses.

Technically these functions and definitions could be written in your main code, but it would make your code very long, possibly create conflicts, and slow things down. The purpose of libraries and dependency files is to take care of a lot of the back-end prep aspects of code so that it can simply be referenced in the main code file.

The Second Library File

After finishing up with epdif.py, we can look at the second and final library file, ep2in13.py , by entering idle ep2in13.py into the terminal. The name refers to the official name of the EPD and contains a lot of driver communication for the display.

In Figure 10-6, the first lines we see are again importing libraries. We can see import ASUS.GPIO as GPIO, since there are some GPIO commands in this file. It also imports the epdif.py file that we just looked at, so it can utilize the functions and previously defined GPIO parameters. It imports the PIL library as well, which we’ll discuss shortly when we look at the demo code.
../images/465130_1_En_10_Chapter/465130_1_En_10_Fig6_HTML.jpg
Figure 10-6

The top of the ep2in13.py file

The other major adjustable item is the screen resolution, defined directly under the library imports. The resolution for this display is set to 128 pixels wide and 250 pixels high. This means that the driver and library consider this to be a vertical display. We’ll want to change the display to show horizontally for our project, but we’ll take care of that in our main code rather than here, since the driver and other library dependencies are written with a vertical orientation in mind.

The rest of the file contains the library commands and functions that make it easier to communicate succinctly with the display, some of which we’ll see shortly in the demo code. You can also reference this file to see if there are any additional functions that will be beneficial to your code or to create your own functions. Many of these will be nested in the epd.init() function as well.

Demo Code

To run the demo code, attach the EPD HAT to the Tinker Board’s GPIO pins and then navigate to the folder with your Waveshare files. For the purposes of the tutorial, let’s assume that they’re in a folder called Waveshare in the Home directory. To navigate to the folder, enter cd /home/linaro/Waveshare into the terminal. Now, to run the demo code, enter sudo python main.py into the terminal. Your e-Paper display should blink black and white three times and then begin showing lines and words, followed by the image seen in the bitmap file, as shown in Figure 10-7.
../images/465130_1_En_10_Chapter/465130_1_En_10_Fig7_HTML.jpg
Figure 10-7

The bitmap file showing on the EPD HAT while the included demo code runs

Running the demo code should give you an idea of what these displays are capable of. Knowing what’s included in the libraries now, let’s take a closer look at main.py to understand how the program code is structured and how we’ll be able to write our own code for our display project.

Open main.py through the terminal with idle main.py. Once it opens, you’ll see that it’s structured very similarly to the Python programs that we wrote in the GPIO chapter, as shown in Figure 10-8. First, we have the dependencies, beginning with epd2in13, which we just looked at, followed by the time library and Image, ImageDraw and ImageFont portions of the PIL library .
../images/465130_1_En_10_Chapter/465130_1_En_10_Fig8_HTML.jpg
Figure 10-8

The main.py demo code file

The PIL library was mentioned briefly when we saw it imported in the ep2in13.py file. PIL is the Python Imaging Library. It’s an image-processing library that allows you to manipulate images in a variety of ways, including editing pixels with the ImageDraw portion of the library. The current form of PIL is actually a fork of the original library called Pillow2 and is widely used in a variety of applications. Beyond the hardware and SPI communication, it’s basically the backbone of the EPD since it allows for the hard-coded images and imported image files to be displayed and controlled.

Then we move on to main(), where the bulk of the code lives. First, epd is defined as epd2in13.EPD(), referring to the library file, and then the display is initialized with a full update by calling epd.init(epd.lut_full_update), which results in the screen flashing black and white a few times when starting main.py.

The next part of the code looks very dense, but if we break it apart it will begin to make sense and not seem so intimidating. This section (Figure 10-9) is where all the items that you see on the screen while main.py is running, except for the bitmap file, are programmed. We’ll be writing similar code for our project, so seeing and understanding this syntax will definitely make things easier.
../images/465130_1_En_10_Chapter/465130_1_En_10_Fig9_HTML.jpg
Figure 10-9

The lines of code that create the first image drawn for the demo code

The first three lines in this block of code define image, draw, and font, which will be called throughout the script. The image parameter is used to refer to everything drawn on the display. For example, if you program three circles and a word to appear together, it is defined as an image and is created by calling image.new. The other parameters defined here are the size of the image (usually matching the resolution listed in the library, as we saw in epd2in13.py) and the color, which is expressed as a number. Here it’s entered as 255, which is white. For this library, 0 will mean black and 255 will mean white. When setting up an image, always call 255 so that the frame can be cleared as commented in the code.

The draw method is called to “draw” the image. By “drawing,” it’s sending data to the screen as interpreted from all the parameters that are called later in conjunction with draw.object() . Finally, font is used to pull a font library from a file directory in TinkerOS to be used for drawing strings. For each font that you use you’ll need to create a new font object. The font size is also defined after the file directory position, as shown in Figure 10-9.

The remaining lines in the block are the items that appear on the screen, programmed one item at a time using draw.rectangle, draw.text, draw.line, draw.arc, and draw.chord, which is a circle. For the shapes and lines, the four numbers in the parentheses are coordinates for their origin and end points using an (x, y, x, y) format. Since they are being “drawn,” you’re essentially telling the PIL library to send the pixels with the coordinates acting as a map. The fill parameter is for color, calling for a number between 0 and 255. The chord and arc shapes have an extra parameter for degrees in a range of 0 to 360.

text is set up a bit differently than the shapes. It only has one set of coordinates, which represent the starting point. This is followed by the string that will be written to the screen. Finally, the font is defined along with the fill color.

Note

Keep track of commas and parentheses when creating these items in your code. The syntax is very important and very particular.

The remainder of main() contains the more technical aspects of the code, controlling how the EPD properly shows the data. The first three commands, clear_frame_memory, set_frame_memory and display_frame() are called immediately after the image is written. The HAT has two memory banks to store the data that it receives from the Tinker Board, and they need to be manually cleared and refreshed. clear_frame_memory completely erases both memory banks and is called only once. set_frame_memory (image, 0, 0) is then called to send the image to the memory banks with 0, 0 acting as coordinates. The final command is display_frame() , which pushes the image from memory to the display.

After the image is pushed to the display, a delay is called with delay_ms(2000). It’s expressed in milliseconds, so the 2000 means 2 seconds. The delay’s purpose is essentially to set the refresh rate for the display, meaning that every 2 seconds the code will update the image by refreshing itself so that any changes enacted will be pushed to the display.

The next line begins this refresh process by calling epd.init(ped.lut_partial_update), which runs a partial update to the display. This happens to subtly refresh the pixels, unlike the rapid flashing of black and white that occurs with the full update called at the beginning of the script. This line sets the stage for a new image to be defined and then called with image.open('monocolor.bmp'), which opens the bitmap file that we’ve seen in the folder. By calling image.open, you can import image files that are in formats supported by PIL.

The next four lines, as shown in Figure 10-10, are what send the bitmap to the display and cause the transition during the demo code from the text and shapes to the bitmap file.
../images/465130_1_En_10_Chapter/465130_1_En_10_Fig10_HTML.jpg
Figure 10-10

The set_frame_memory() commands after a new image is created

This time, set_frame_memory (image, 0, 0), with image referring to the newly imported bitmap, and display_frame() are both called twice. As mentioned previously, the EPD has two memory banks, and by calling both of those commands twice, we make sure each bank receives the new image to display and updates. If both of these were only called once, it would cause the display to be glitchy; and if they weren’t called at all, the display would not update. We didn’t need to call them twice when the original image was sent, because the memory had been fully cleared using clear_frame_memory .

The final portion of the code is possibly formatted incorrectly, depending on the intent of the developer. As you can see in Figure 10-11, an additional new image is created, this time called time_image . After the parameters for this new image are set, we enter a loop using while True: that will display a rectangle with some text meant for time_image. The text is formatted to be a digital clock using time.strftime('%M:%S'). Using strftime() from the time library allows you to display real-time time data, such as a date, or in this case a clock, using custom formatting with % signs and letters, as shown with ('%M:%S'). We’ll discuss this concept further when we write the code for our project.

Because we placed the time.strftime() in the loop, it will update rather than remaining static as it would when called outside the loop. However, as discussed, for the EPD to display an updated image, both memory banks need to be reset. Because that is absent from the loop, the clock will not display, even though display_frame() is called. Instead, as you see when running the demo code, the bitmap image remains statically on the display.

It’s possible that it was intended to show that you could utilize the EPD as a clock display, but the developers also wanted to keep the bitmap displayed for the purposes of the demo code. It’s just important to note that if you were to set up a clock display, you would need to refresh the memory inside of the loop.

After going through the demo code, you should now have a better understanding of how the EPD works from a hardware and software perspective. There are quite a few steps to program a fully functional e-Paper display, but luckily with the various libraries and examples we’re well on our way to creating our own version. With that foundation, let’s move on to laying the groundwork for our project, which will involve drawing our own display image from scratch and displaying information that updates in real time, including the weather forecast using the pyowm library.

OpenWeatherMap and pyowm

Now that we’re acquainted with the EPD’s library and coding architecture, we can look at the pyowm coding library. pyowm is the Python wrapper for OpenWeatherMap (OWM), which is a weather API that provides access to weather and forecast data from around the world. OWM is accessed through its web site,3 where you can create a free or paid account. The free account has all the features needed for this project.

With the account comes an API key, which you’ll need in your Python code. You can access your API key through your OWM account settings under the API tab, as shown in Figure 10-11.
../images/465130_1_En_10_Chapter/465130_1_En_10_Fig11_HTML.jpg
Figure 10-11

The API tab on OWM’s web site. The API key is hidden for privacy concerns. Never share your API key publicly.

It’s recommended to log in to OWM on the Tinker Board so that you can copy and paste your API key into your code instead of trying to type it, since as you’ll see it’s quite long and a random assortment of letters and numbers.

After you have your key, the next step will be to install the Python library on the Tinker Board. pyowm is installed using pip, so with the terminal navigate to the Home directory using cd ~ and then enter pip install pyowm.

With the library installed, you can start using it to access weather data in Python. The main process for using pyowm is to enter your API key, declare what city you want to collect data for, and then list the types of data that you want. There is an almost endless amount of data available, ranging from the broad to the detailed. The syntax for all of these functions is well documented on the pyowm project’s GitHub.4

The cities are imported by including either the city name with the country code, which is a two-letter abbreviation, or the registry ID, which is a number code assigned to the city and stored in the OWM city registry, which you have access to with the library. These can be accessed in a Python script that we’ll go over now and will also be available on the GitHub repo for this book.

Getting Your City ID

Start by opening a new Python file with IDLE and import the pyowm library with this statement:
import pyowm
Then, enter your API key with this:
owm = pyowm.OWM('api_key')

This means that every time owm is listed, it’s referring to your API key. This way, if a feature is not available for your account level, then you won’t be able to pull the data. In our case, this shouldn’t be a problem.

Now we’re going to call up the registry for city IDs:
registry = owm.city_id_registry()
And then create a variable called results that we’ll be able to print with whichever city is looked up. We’ll do that with this code:
results = registry.ids_for('city_name')
You’ll be able to put any city name into the parentheses between the quotation marks. We’ll then print the results to the terminal:
print(results)
Don’t add quotation marks around results, because it isn’t a string. We want to print the data stored in results. So, to recap, here is our complete script to gather city IDs:
import pyowm
owm = pyowm.OWM('api_key')
registry = owm.city_id_registry()
results = registry.ids_for('city_name')
print(results)
Once you enter your city’s name into the parentheses as a string, run the script either in the Python shell or with TinkerOS’s terminal and make note of your city ID and/or country abbreviation. You’ll need it for our eventual project script. If your city is the name of a city in multiple countries, then all of them will be listed when you run the script, as shown in Figure 10-12 with Toronto, Los Angeles, and Melbourne.
../images/465130_1_En_10_Chapter/465130_1_En_10_Fig12_HTML.jpg
Figure 10-12

The results of a few example cities displayed in the Python shell that were queried with our script to find city IDs

Python Script for the Weather Display

Now that we have introduced both the EPD’s Python library and the concepts behind the pyowm library, we can begin writing the code for our project. As mentioned earlier, the goal of this project is to use the EPD to display the date, time, and current weather; specifically, the temperature and current forecast. This information can be displayed in a variety of ways since the EPD is essentially a blank canvas.

For our purposes, we’re going to build up a grid where the date and time will be at the top of the display and then the bottom two-thirds of the display will have four boxes. Two of them of them will be on the left side and say “Weather:” and “Temperature:” with the remaining two boxes directly across on the right side receiving the OWM data for current forecast and temperature. When everything is finished, it will look like Figure 10-13.
../images/465130_1_En_10_Chapter/465130_1_En_10_Fig13_HTML.jpg
Figure 10-13

The finished EPD weather display running on the Tinker Board S. You can also use the original Tinker Board as well.

We’ll begin the Python script as we have with all the others: importing library dependencies. We’ll need the PIL library , the epd2in13 Python file, the time library , the math library , and of course the pyowm library . After importing these, we’ll begin to declare some of these libraries’ objects, beginning with epd from epd2in13.py as we saw in the demo code for the EPD. We’ll also bring in the pyowm API key, specific to your account. As a result, the beginning of our code will be written like this:
import epd2in13
import time
from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont
import pyowm
import math
epd = epd2in13.EPD()
owm = pyowm.OWM('api_key')
Next, we go into the main() function where we’ll fully update the display as we saw in the demo code for the EPD display:
epd.init(epd.lut_full_update)

After this, we’ll go into the loop. Because everything on our display will need to be updated in real time, we’ll draw the image and pull the data in the loop. We’ll use while True: to begin the loop and then declare objects and variables for the pyowm library.

First, we’ll declare where the weather data will be collected from, with weather_at_place(), and we’ll also create w = observation.get_weather(), which will be the variable to gather weather data for the pyowm library . Both will be called in the loop so that new data is constantly being gathered; otherwise, we would get the data that was originally pinged when the script was first started. The command w.get_status() is used to get the forecast. This will show things like clear, rain, clouds, snow, and so on. We’re also going to print this to the console for troubleshooting purposes with print w.get_status(). Since w.get_status() outputs a string, it doesn’t need to be modified when used with print.

Note

If you want to use the city ID number instead, you’ll use weather_at_id() rather than weather_at_place()

Next, we’re going to take care of the temperature data. First, we’ll call w.get_temperature('fahrenheit')['temp'], which is the temperature equivalent to get_status(), but with a twist. By putting fahrenheit in the parentheses, the temperature will be read in Fahrenheit, however you can also use Celsius (or even Kelvin), which of course is much more common around the world.

get_temperature() returns a dictionary of temperature data and for our purposes we only need the current temperature, which is cataloged as ['temp'], which is why it’s being called next to get_temperature(). The other issue with ['temp'] is that it has a decimal attached to it, which for the display we’ll want to cut off to show just the base temperature.

To parse this piece of data separately, we’re going to use the math library that we imported earlier to use math.trunc. This completely cuts off the decimal of any number, leaving us with just the whole number; which in this case is the temperature.

The only remaining issue is that math.trunc creates an integer, which can’t be printed with the PIL draw.text() function ; it requires a string. Luckily, Python has an easy way to convert an int to a string, by placing str next to the int inside parentheses. We’re going to create a new variable called temp to hold the string output for this. Finally, for troubleshooting purposes, we’ll also print this data to the terminal with print temp. Altogether, the code will look like this:
      while True:
             observation = owm.weather_at_place('New York,US')
             w = observation.get_weather()
             w.get_status()
             print w.get_status()
             w.get_temperature('fahrenheit')['temp']
             temp = str(math.trunc(w.get_temperature('fahrenheit')['temp']))
             print temp
After the data, we go into the portion of the code where we draw the image. First, much as in the demo script, we’re going to declare a new image object. However, one difference this time is that we’re going to change the width and height parameters so that the width will be 250 pixels with a height of 128 pixels. This way we can use the display horizontally instead of vertically. We’ll then bring in the draw object and font objects as before. A change for the fonts, though, is that we’ll be using three different fonts: FreeSans, FreeSansBold and FreeSansBoldOblique. We’ll create a new font object for each and name them font, font2, and font3. These lines will be written as follows:
             image = Image.new('1', (250, 128), 255)
             draw = ImageDraw.Draw(image)
             font = ImageFont.truetype('/usr/share/fonts/truetype/freefont/FreeSans.ttf', 24)
             font2 = ImageFont.truetype('/usr/share/fonts/truetype/freefont/FreeSansBold.ttf', 24)
             font3 = ImageFont.truetype('/usr/share/fonts/truetype/freefont/FreeSansBoldOblique.ttf', 18)
Next, we’ll draw our first item, a rectangle, to give a slight border around the display:
draw.rectangle((0, 0, 250, 128), fill = 255)

The rectangle will be followed by our first text items: the date and time. To do this, we’ll use the time.strftime() function to grab this data in real time. The other aspect of these text entries is the location on the EPD. As discussed, we want these two items to be at the top of the screen and also to be in line with each other. The easiest way to think of this is to see the X axis parameter as asking “how many pixels away from the left side of the screen do you want it to be?” and the Y axis parameter as “how many pixels down from the top of the screen do you want it to be?” This will take some experimentation, since it will all be affected by the font, font size, and length of the string.

For the date, we’ll enter
             draw.text((7, 5), time.strftime('%a. %b. %d, %Y'), font = font3, fill = 0)

For time.strftime('%a. %b. %d, %Y'), the % followed by letters all express different date parameters. In this example, %a will give us the abbreviation for the day of the week, %b will display the abbreviation for the month, %d will give us the number date and %Y will give us the year with the century number. Capitalization does count with this syntax, and you can find more information on all the items available in the time library’s documentation.5

The line for the time, which will basically be a digital clock, will have a similar format to the date:
             draw.text((170, 5), time.strftime('%l:%M%p'), font = font3, fill = 0)

Just as with the date, time.strftime() is used again for the clock, but with different parameters. Here %l is used for the hour, followed by : and then %M is used for the minutes, and %p gives us either AM or PM. Just as with the date, there are more parameters available that are applicable to time-keeping.

For the coordinates, notice that the Y coordinate is the same for both the date and time. This allows them to be lined up with each other.

Now we’re going to draw the first lines of our grid: first a line underneath the date and time and then a second line dividing the spaces for the date and time. To do this, we’ll write
draw.line((250, 25, 0, 25), fill = 0)
draw.line((170, 0, 164, 25), fill = 0)

For the lines, there are two sets of X and Y coordinates. To make it easier to visualize, think of it as finding two points on the display and then drawing the line between them. The first line we draw is going all the way across horizontally below the date and time. The second line is a bit different because it isn’t straight, it’s slanted; resulting in different X values for each coordinate. This is to match the fact that the font we’ve chosen is italicized. The value 25 is used as the second Y coordinate so that it lines up with the horizontal line.

With that part of the image complete, we’re going to move on to the forecast and temperature entries. For the forecast we want to display "Weather:" with the output of get_status() from pyowm next to it. We’ll do this with the following lines:
draw.text((30, 35), 'Weather:', font = font2, fill = 0)
draw.text((170, 35), w.get_status(), font = font, fill = 0)

You’ll notice that the Y coordinate is the same for both entries, just as with our date and time data, so that they’ll be lined up. There’s also some space allowed below the previously drawn horizontal line. By calling w.get_status() as the string, we make sure the forecast data output will be constantly updated.

After the forecast is the temperature, where we’ll be displaying "Temperature :" followed by the current temperature data. To do this, we’ll need two draw.text() entries:
draw.text((5, 85), 'Temperature:', font = font2, fill = 0)
draw.text((182, 85), temp + chr(176) + 'F', font = font, fill = 0)

Much like the way we used w.get_status() for the current forecast, we’re doing a similar thing for temperature, where we’re using the temp variable that we wrote earlier to hold the get_temperature() function , which has been broken down to just show the ['temp'] category without the decimal point as a string.

Additionally, this line holds the degree sign and "F" for Fahrenheit. The degree sign is obviously not freely available on a standard keyboard, so instead we’re using Unicode to insert it using chr(176), which is the Unicode character number for the degree sign. The °F is placed next to the temperature data, so that it will always be directly to the right of the temperature, whether it is a single-digit or triple-digit (yikes!) number. Both lines are placed at the same Y coordinate.

Finally, to complete our image, we’re going to draw two more lines, one to divide the text from the data and one to divide the forecast section from the temperature section:
draw.line((250, 70, 0, 70), fill = 0)
draw.line((164, 25, 164, 128), fill = 0)

The horizontal line crosses directly between the temperature and forecast sections, and the vertical line begins at the same coordinate (25y) as the first horizontal line so that it looks like they’re crossing. This vertical line is straight, unlike the first vertical line, which was diagonal, and crosses the second horizontal line. These lines complete our image so that we can move on to writing it to the memory banks of the EPD hat.

The Technical Parts of the Script

This portion of our code will resemble the demo script, since we’ll have to follow the same process of clearing and writing to the memory. First, we’ll clear the memory and then set the frame memory, followed by a call to display the image. A difference is with the set_frame_memory() function . Because we’re rotating the display to use it horizontally, we need to transpose the image by 270 degrees. As a result, these three lines will be written as:
epd.clear_frame_memory(0xFF)
epd.set_frame_memory(image.transpose(Image.ROTATE_270), 0, 0)
epd.display_frame()
Next, we’ll add our 2-second delay, followed by a partial display update:
epd.delay_ms(2000)
epd.init(epd.lut_partial_update)
And finally, we’ll reset both memory banks to fully write our updated image with our constantly updating data:
epd.set_frame_memory(image.transpose(Image.ROTATE_270), 0, 0)
epd.display_frame()
epd.set_frame_memory(image.transpose(Image.ROTATE_270), 0, 0)
epd.display_frame()
Putting that together, our final code with all the bells and whistles will look like this:
#!/usr/bin/env python
import epd2in13
import time
from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont
import pyowm
import math
epd = epd2in13.EPD()
owm = pyowm.OWM('insert_API_key_here')
def main():
    epd.init(epd.lut_full_update)
    while (True):
        observation = owm.weather_at_place('New York,US')
        w = observation.get_weather()
        print w.get_status() #  print forecast status
        w.get_temperature('fahrenheit')['temp']
        temp = str(math.trunc(w.get_temperature('fahrenheit')['temp']))
        print w.get_temperature('fahrenheit')
        image = Image.new('1', (250, 128), 255)
        draw = ImageDraw.Draw(image)
        font = ImageFont.truetype('/usr/share/fonts/truetype/freefont/FreeSans.ttf', 24)
        font2 = ImageFont.truetype('/usr/share/fonts/truetype/freefont/FreeSansBold.ttf', 24)
        font3 = ImageFont.truetype('/usr/share/fonts/truetype/freefont/FreeSansBoldOblique.ttf', 18)
        draw.rectangle((0, 0, 250, 128), fill = 255)
        draw.text((7, 5), time.strftime('%a. %b. %d, %Y'), font = font3, fill = 0)
        draw.text((170, 5), time.strftime('%l:%M%p'), font = font3, fill = 0)
        draw.line((250, 25, 0, 25), fill = 0)
        draw.line((170, 0, 164, 25), fill = 0)
        draw.text((30, 35), 'Weather:', font= font2, fill = 0)
        draw.text((170, 35), w.get_status(), font = font, fill = 0)
        draw.text((5, 85), 'Temperature:', font = font2, fill = 0)
        draw.text((182, 85), temp + chr(176) + 'F', font = font, fill = 0)
        draw.line((164, 25, 164, 128), fill = 0)
        draw.line((250, 70, 0, 70), fill = 0)
        epd.clear_frame_memory(0xFF)
epd.set_frame_memory(image.transpose(Image.ROTATE_270), 0, 0)
        epd.display_frame()
        epd.delay_ms(2000)
        epd.init(epd.lut_partial_update)
epd.set_frame_memory(image.transpose(Image.ROTATE_270), 0, 0)
        epd.display_frame()
epd.set_frame_memory(image.transpose(Image.ROTATE_270), 0, 0)
        epd.display_frame()
if __name__ == '__main__':
    main()

Although the commands are fairly straightforward for displaying to the EPD, getting the placement and sizing just right can take a lot of experimentation and as a result time. However, you truly have a blank canvas to fully customize your project and have it do exactly what you want. There’s just one more step to take this project to the next level…

Autorun Setup

Now that our code is written and working, we’re going to configure it to run automatically when the Tinker Board boots up. This way, you can run this project without having to attach a keyboard, mouse, or display. It can be a standalone Internet-connected device, commonly referred to as headless.

However, the fact that it requires an Internet connection does mean that some special considerations must be made when setting this up. The code will not execute without an active connection, because of the pyowm library for our weather data, so it needs to be configured to execute only after the Tinker Board has booted up and there is also an active network connection present.

To do this, we’ll set up our Python script to be executable, so that it can launch on its own. Then we’ll place the script and its dependencies into a folder found under /etc/network called if-up.d. This folder contains scripts that automatically run once a network connection is established (thus the naming convention “if-up”). The only caveat here is that this folder requires root access. Files cannot simply be dragged and dropped into it. For that reason, we’ll be doing this work through the terminal.

Note

Although everything is being done through the terminal, it’s recommended to have the GUI file directory open to the if-up.d folder to double-check that everything is working properly.

First, we’ll make our Python script an executable file. We went over this process briefly in the GPIO chapter while discussing the ways to run our Python code. We need to add the following line to the top of our script:
#!/usr/bin/env python

Save your script and then open a terminal, changing directories to your project’s folder, and enter chmod a+x weatherDisplay.py. Nothing will seem to happen in the terminal, but if you click the file in the GUI, you’ll be given the choice to run the script without any other commands. You can also run it via the terminal with sudo ./weatherDisplay.py.

Speaking of our script’s name, we need to remove the .py extension for this method to work properly. We can do this easily in the GUI by right-clicking the file and renaming it. We need to do this because any files that have an extension like that will not run in the if-up.d folder.

Now that the prep work is taken care of, we can begin the process of moving everything to the if-up.d folder. As mentioned previously, this folder is only accessible as root. To use the terminal as root enter sudo su. This automatically brings you back to the Home directory by default. You’ll be able to confirm that you are root by checking the terminal, which should show root@tinkerboard , as in Figure 10-14.
../images/465130_1_En_10_Chapter/465130_1_En_10_Fig14_HTML.jpg
Figure 10-14

root@tinkerboard in the terminal

As root, change directories to the if-up.d folder with cd /etc/network/if-up.d. Again, it’s recommended to double-check your work by having the file directory GUI opened to the same if-up.d folder. Now we’re going to copy the project folder, which for the purposes of this example will be called projectFolder. The projectFolder will need to contain the script and library dependencies. We’ll copy it into the if-up.d folder using the following terminal command
cp -r /home/linaro/projectFolder /etc/network/if-up.d.

Note

You may need to place copies of the Python libraries, including pyowm, into the projectFolder depending on how you’ve set up Python and pip directories on your Tinker Board. It will also depend on how future iterations of TinkerOS and the kernel handle this as well.

Basically, you’re using the copy command, cp, and then listing the file location of the folder you wish to copy, followed by a space, and then the file location of the folder where you want to copy your original folder to; also known as the target folder.

After executing the command, you should see your project folder appear in the if-up.d folder in the GUI. The next step is to get all the files out of the project folder and directly into the if-up.d folder. We could have copied each individual file over, but by copying the folder we can perform this task more succinctly by moving the contents of the file folder up one level in the file directory using the mv command . Enter
mv /etc/network/if-up.d/projectFolder/* /etc/network/if-up.d/projectFolder/.* .

Note the space between the last * and . at the end of the line. By entering the file extension of the project folder with that syntax, you ensure that all folders and files are moved up a level, even if their file names begin with a period (.). Omitting the space could cause errors; especially when dealing with code library files that may use unique naming conventions. After executing the command, you should see all the files and folders from the project folder appear directly into the if-up.d folder.

Everything should be ready to go, but first we need to clean things up a bit. The project folder is empty now and is no longer necessary, so we should remove it. We’ll do that using the rm (remove) command. This is a very powerful command; once you execute it, it will immediately remove the targeted file or folder, so always be cautious when using it.

The rm command has different flags for whether you are deleting a single file or an entire folder. The flag for a folder, or directory, is -r. Also, instead of typing out the entire file directory location, you’ll only include the name of the folder you’re removing. So, the command to remove the now empty project folder inside if-up.d is rm -r projectFolder. You should see the folder disappear from the file directory GUI.

Before we reboot to see if our script starts up correctly with a network connection, we should test to make sure the script can run properly from the if-up.d folder. Run the script in the terminal using ./weatherDisplay (no sudo is needed, since we’re still root) to make sure that no errors appear. If you get any missing dependency errors, then you’ll need to copy those library folders, as mentioned previously in the note, over to the if-up.d folder. You can use the same cp process for this that we went over earlier.

If the EPD displays as expected, then it’s time to reboot the Tinker Board using reboot. If all goes as planned, then after TinkerOS boots to the desktop with a successful network connection you should see the EPD begin the script by doing the full update and then updating the date, time, and weather in real time.

Finishing Touches

With all the coding and Linux work done, you have a fully functional Internet-connected information display. Because it is headless, you can place it anywhere with a connection to power, and have it run. There are some aesthetic things you can do, though, to bring it to the next level.

Instead of simply leaving the Tinker Board with the EPD display exposed to the elements, you can put it in a housing of some sort. As we discussed in the first chapter, there are a variety of cases available that will fit the Tinker Board’s form factor. Many of these cases have openings to allow access to the GPIO pins and as a result, the Tinker Board could be protected from environmental and physical hazards while having the EPD display remain attached and unaffected by the housing.

There are also fully DIY options out there with the only limitation being your imagination. There’s always the option of 3D printing or milling a custom case. Another popular option for EPD projects is to utilize a picture frame, or similarly functional household item, to allow the display to blend in with its surroundings in a home.

Of course, you aren’t just limited to showing the data parameters that we coded for in this project. These displays can basically show any information that you want. There are many calendar, news, and social media APIs available for Python that you can utilize in your code to fully customize the kind of EPD project that you want. You can also animate these displays further with the PIL library and integrate them with larger projects.

This chapter has shown how you can integrate programming with the Tinker Board to create fully customized projects and given you the tools to go further with your own ideas; which has truly been the goal of this entire book. We have one more project in front of us, and it is a classic: a robot!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.221.98.71