The first few lines of code.py just import all the libraries we'll be using.

import time
import math
import board
import busio
from digitalio import DigitalInOut
import neopixel
from adafruit_esp32spi import adafruit_esp32spi
from adafruit_esp32spi import adafruit_esp32spi_wifimanager
import displayio
import adafruit_touchscreen
import adafruit_imageload

Then, the touchscreen is set up and calibrated, creating the ts  object we'll be using to find where the user is pressing.

ts = adafruit_touchscreen.Touchscreen(
    board.TOUCH_XL,
    board.TOUCH_XR,
    board.TOUCH_YD,
    board.TOUCH_YU,
    calibration=((5200, 59000), (5800, 57000)),
    size=(320, 240),
)

Now, we set up the WiFi connection. Make sure you have the latest version of CircuitPython installed since this will definitely not work on older versions.

try:
    from secrets import secrets
except ImportError:
    print("WiFi secrets are kept in secrets.py, please add them there!")
    raise
 
# If you are using a board with pre-defined ESP32 Pins:
esp32_cs = DigitalInOut(board.ESP_CS)
esp32_ready = DigitalInOut(board.ESP_BUSY)
esp32_reset = DigitalInOut(board.ESP_RESET)
 
spi = busio.SPI(board.SCK, board.MOSI, board.MISO)
esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset)
 
status_light = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2)
 
wifi = adafruit_esp32spi_wifimanager.ESPSPI_WiFiManager(esp, secrets, status_light)

Now we declare the IP of the Roku. You can find this by going to Settings -> Network -> About. In my case it was 192.168.1.3, but yours will probably be different. The keypress commands you can send to the Roku are listed out. See the 'Keypress key values' section of the Roku ECP documentation for more info about these.

# Set the ip of your Roku here
ip = "192.168.1.3"

"""
Possible keypress key values to send the Roku
Home
Rev
Fwd
Play
Select
Left
Right
Down
Up
Back
InstantReplay
Info
Backspace
Search
Enter
FindRemote
 
Keypress key values that only work on smart TVs with built-in Rokus
VolumeDown
VolumeMute
VolumeUp
PowerOff
ChannelUp
ChannelDown
"""

This is the first of four helper functions. This function gets the XML with the installed channels and their IDs. See the 'General ECP Commands' section of the Roku ECP documentation for more info about this.

def getchannels():
    """ Gets the channels installed on the device. Also useful because it
        verifies that the PyPortal can see the Roku"""
    try:
        print("Getting channels. Usually takes around 10 seconds...", end="")
        response = wifi.get("http://{}:8060/query/apps".format(ip))
        channel_dict = {}
        for i in response.text.split("<app")[2:]:
            a = i.split("=")
            chan_id = a[1].split('"')[1]
            name = a[-1].split(">")[1].split("<")[0]
            channel_dict[name] = chan_id
        response.close()
        return channel_dict
    except (ValueError, RuntimeError) as e:
        print("Failed to get data\n", e)
        wifi.reset()
        return None
    response = None
    return None

This function sends keypresses to the Roku. It sends commands for stuff like navigation and media control. It takes a string as input. See the 'General ECP Commands' section of the Roku ECP documentation for more info about this.

def sendkey(key):
    """ Sends a key to the Roku """
    try:
        print("Sending key: {}...".format(key), end="")
        response = wifi.post("http://{}:8060/keypress/{}".format(ip, key))
        if response:
            response.close()
            print("OK")
    except (ValueError, RuntimeError) as e:
        print("Failed to get data\n", e)
        wifi.reset()
        return
    response = None

This function isn't actually used, but if it were, it could be used to send characters when the on-screen keyboard is active. It takes a character as input. See the 'General ECP Commands' section of the Roku ECP documentation for more info about this.

def sendletter(letter):
    """ Sends a letter to the Roku, not used in this guide """
    try:
        print("Sending letter: {}...".format(letter), end="")
        response = wifi.post("http://{}:8060/keypress/lit_{}".format(ip, letter))
        if response:
            response.close()
            print("OK")
    except (ValueError, RuntimeError) as e:
        print("Failed to get data\n", e)
        wifi.reset()
        return
    response = None

Openchannel sends the Roku the ID associated with the channel you'd like to open, and the Roku then opens said channel. It takes an integer as an input. See the 'General ECP Commands' section of the Roku ECP documentation for more info about this.

def openchannel(channel):
    """ Tells the Roku to open the channel with the corresponding channel id """
    try:
        print("Opening channel: {}...".format(channel), end="")
        response = wifi.post("http://{}:8060/launch/{}".format(ip, channel))
        if response:
            response.close()
            print("OK")
        response = None
    except (ValueError, RuntimeError):
        print("Probably worked")
        wifi.reset()
    response = None

This function takes in a tuple of the page you would like to go to and the tile grid of the bitmap for that page and then switches you to that page. It wasn't possible to pass in two values since they were stored in a list, which is why it takes a tuple. It returns two new lists that are used to determine which functions run when a button gets pressed and what values get sent to the functions.

def switchpage(tup):
    """ Used to switch to a different page """
    p_num = tup[0]
    tile_grid = tup[1]
    new_page = pages[p_num - 1]
    new_page_vals = pages_vals[p_num - 1]
    my_display_group[-1] = tile_grid
    return new_page, new_page_vals

This line isn't really necessary, but it does serve a useful purpose. It verifies that everything is still connected to WiFi and you can communicate with the Roku. Additionally, the first communication you make with the Roku using the ECP takes a very long time, so this takes care of that so the remote is ready to go as soon as the images show up on the screen.

# Verifies the Roku and Pyportal are connected and visible
channels = getchannels()

Now, we set up displayio. We load 3 different bitmaps and then create a TileGrid object for each of them.

my_display_group = displayio.Group(max_size=25)
 
image_1, palette_1 = adafruit_imageload.load(
    "images/page_1.bmp", bitmap=displayio.Bitmap, palette=displayio.Palette
)
tile_grid_1 = displayio.TileGrid(image_1, pixel_shader=palette_1)
my_display_group.append(tile_grid_1)
 
image_2, palette_2 = adafruit_imageload.load(
    "images/page_2.bmp", bitmap=displayio.Bitmap, palette=displayio.Palette
)
tile_grid_2 = displayio.TileGrid(image_2, pixel_shader=palette_2)
 
image_3, palette_3 = adafruit_imageload.load(
    "images/page_3.bmp", bitmap=displayio.Bitmap, palette=displayio.Palette
)
tile_grid_3 = displayio.TileGrid(image_3, pixel_shader=palette_3)

If you want to change what buttons do what, this is the place to change it. The lists are organized so every value is in the same row and column visually as it is on the display. page_x stores what functions are run when that part of the screen is pressed and page_x_vals stores the values to be passed to those functions when they are run.

For example, say I wanted to open Netflix when I pressed in the upper right-hand corner on the first page, I'd change the third value of page_1 to openchannel and change the third value of page_1_vals to 12.

page_1 = [sendkey, sendkey, sendkey,
          sendkey, sendkey, sendkey,
          sendkey, sendkey, sendkey,
          sendkey, sendkey, switchpage]
 
page_1_vals = ["Back", "Up", "Home",
               "Left", "Select", "Right",
               "Search", "Down", "Info",
               "Rev", "Play", (2, tile_grid_2)]
 
# Page 2
page_2 = [openchannel, openchannel, openchannel,
          openchannel, openchannel, openchannel,
          openchannel, openchannel, openchannel,
          switchpage, sendkey, switchpage]
 
page_2_vals = [14362, 2285, 13,
               12, 8378, 837,
               38820, 47389, 7767,
               (1, tile_grid_1), "Home", (3, tile_grid_3)]
 
page_3 = [None, None, None,
          sendkey, None, sendkey,
          sendkey, sendkey, sendkey,
          switchpage, sendkey, sendkey]
 
page_3_vals = [None, None, None,
               "Search", None, "Info",
               "Rev", "Play", "Fwd",
               (2, tile_grid_2), "Back", "Home"]

The first two lists in the following lines of code contain the lists discussed in the previous section. This just makes it easier to switch between the sets of commands based on what page is active.

The next two lines set page to page_1 and page_vals to page_1_vals. This makes the first page open by default. These variables are used to store the function and value lists for the active page.

After that, the display starts showing the tile grid, and we print ready. The latter statement is really only useful for when you have a serial terminal open.

pages = [page_1, page_2, page_3]
pages_vals = [page_1_vals, page_2_vals, page_3_vals]
 
page = page_1
page_vals = page_1_vals
 
board.DISPLAY.show(my_display_group)
print("READY")

Main loop

last_index = 0
while True:
    p = ts.touch_point
    if p:
        x = math.floor(p[0] / 80)
        y = abs(math.floor(p[1] / 80) - 2)
        index = 3 * x + y
        # Used to prevent the touchscreen sending incorrect results
        if last_index == index:
            if page[index]:
                # pylint: disable=comparison-with-callable
                if page[index] == switchpage:
                    page, page_vals = switchpage(page_vals[index])
                else:
                    page[index](page_vals[index])
                time.sleep(0.1)
 
        last_index = index

Right before the main loop starts, last_index is set to 0. last_index is used so that you need to press on the button for at least two loops for it to activate. This barely affects responsiveness and drastically decreases the chance of an incorrect read.

Then, the loop is started, and we define p as where the Touchscreen object, ts is detecting a press.

last_index = 0
while True:
    p = ts.touch_point

If p is None, so if there aren't any detected presses, the loop simply starts again.

However, if p is not None, so if the board is detecting the touch screen is being pressed, it continues to the next section of the loop.

To make things easier on myself, I simply converted the touch screen x and y values ranging from 0-320 and 0-240, respectively, to values ranging from 0-3 in the x-direction and 0-2 in the y-direction. I reverse y by taking the absolute value of the value ranging from 0-2 and subtracting 2.

Now, index is created. The 3*x is how many rows up to go, since each row has 3 values in it, and the y is how many columns over to go.

if p:
    x = math.floor(p[0] / 80)
    y = abs(math.floor(p[1] / 80) - 2)
    index = 3 * x + y

The last section of the loop runs the commands from the list. I've added line numbers so it will be easier to explain.

line 1 just verifies that the current button has been pressed twice without a different one being pressed. If that is true, we then check if that button even has a function connected to it. If it doesn't, the loop restarts. Then, on line 4, we see if the user is intending to switch the page, since that function returns values that need to be assigned to variables, unlike the other functions. If that is not the case, then we simply run the function in page[index] and pass it page_vals[index]. If a button has been pressed, we wait 0.1 seconds so you don't accidentally press it more than intended, and then set last_index to the current index.

if last_index == index:
    if page[index]:
        # pylint: disable=comparison-with-callable
        if page[index] == switchpage:
            page, page_vals = switchpage(page_vals[index])
        else:
            page[index](page_vals[index])
        time.sleep(0.1)

    last_index = index

This guide was first published on Jul 09, 2020. It was last updated on Jul 09, 2020.

This page (Code Run Through) was last updated on Feb 19, 2021.

Text editor powered by tinymce.