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() 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.root_group = my_display_group print("READY")
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
Page last edited March 08, 2024
Text editor powered by tinymce.