Code Breakdown

Now we will have a look at the critical parts of the CircuitPython code so you know how it works and how you can change things to make it work the way you want it to. We are going to skip over a few sections that handle things like WiFi connection via the on-board ESP32 and other things that need to be included but are best not changed.

There are plenty of places for you to customize the layout of this display and that is what this section is all about.

Sensor Setup

The PyPortal has a temperature and light sensor already attached to it as well as two IO connectors. One of these connectors will have a PIR Sensor attached to detect motion.

Here a few global variables are set up that represent sensor readings and button states for later use. For input data, we will be getting information from the ADT7410 Temperature sensor, the onboard light sensor, PIR motion sensor, and two touchscreen buttons.

Download: file
# ------- Sensor Setup ------- #
# init. the temperature sensor
i2c_bus = busio.I2C(board.SCL, board.SDA)
adt = adafruit_adt7410.ADT7410(i2c_bus, address=0x48)
adt.high_resolution = True
temperature = ""
# init. the light sensor
light_sensor = AnalogIn(board.LIGHT)

# init. the motion sensor
movement_sensor = DigitalInOut(board.D3)

button1_state = 0
button2_state = 0

You can add more inputs using the D4 connector or connecting to the four pin i2c bus. Then simply add the setup variables to this section.

Bitmap Fonts

This section will let you load a bitmap font and pre-load the letter and number glyphs to speed up information updates.

The font can be changed out with any other font but you will need to update the file path for your font in the following line of code:

font = bitmap_font.load_font("/fonts/Helvetica-Bold-16.bdf")

For this example I have created a folder named fonts that contains the Helvetica-Bold-16.bdf file.

Download: file
# ---------- Set the font and preload letters ---------- 
# Be sure to put your font into a folder named "fonts".
font = bitmap_font.load_font("/fonts/Helvetica-Bold-16.bdf")
# This will preload the text images.
font.load_glyphs(b'abcdefghjiklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890- ()')

User interface Elements

Since we have many types of information to display, we will make use of Groups and Sub Groups to keep track of all our UI Elements. This will basically let us set up a single group with elements that can be displayed all at once.

The group that was set up for this example is called splash and it is rendered on the screen when the following code is called:

board.DISPLAY.show(splash)

Elements can be added to this group with the following code where yourElement is replaced with the name of the element you are adding.

splash.append(yourElement)

You can also change the background color by changing the HEX color code assigned to color_palette[0].

Download: file
# ------------- User Inretface Eliments ------------- #

# Make the display context
splash = displayio.Group(max_size=200)
board.DISPLAY.show(splash)

# Make a background color fill
color_bitmap = displayio.Bitmap(320, 240, 1)
color_palette = displayio.Palette(1)
color_palette[0] = 0x3D0068
bg_sprite = displayio.TileGrid(color_bitmap, x=0, y=0,
                               pixel_shader=color_palette)
splash.append(bg_sprite)

Display Buttons

This example sets up two buttons that will react when touched. These buttons will be used to send signals to Home Assistant via MQTT. Each button is added to a Subgroup named buttons. This is then added to the splash Group.

The Button element accepts parameters for the x and y position along with the height, width, label, label_font, label_color, fill_color, outline_color, and style.

First we set some style variables for the BUTTON_WIDTH, BUTTON_HEIGHT, and BUTTON_MARGIN.

Then we define the button object using Button() and filling in the appropriate parameters.

Once the buttons are all set up with unique names, they are added to the buttons group and that group is added to the splash group.

Download: file
buttons = []
# Default button styling:
BUTTON_WIDTH = 100
BUTTON_HEIGHT = 100
BUTTON_MARGIN = 10

# Button Objects
button_1 = Button(x=BUTTON_MARGIN, y=BUTTON_MARGIN,
                  width=BUTTON_WIDTH, height=BUTTON_HEIGHT,
                  label="Button 1", label_font=font, style=Button.SHADOWROUNDRECT, label_color=0x505050,
                  fill_color=0x9e9e9e, outline_color=0x464646)
buttons.append(button_1)

button_2 = Button(x=BUTTON_MARGIN, y=BUTTON_MARGIN*2+BUTTON_HEIGHT,
                  width=BUTTON_WIDTH, height=BUTTON_HEIGHT,
                  label="Button 2", label_font=font, style=Button.SHADOWROUNDRECT, label_color=0x505050,
                  fill_color=0x9e9e9e, outline_color=0x464646)
buttons.append(button_2)

for b in buttons:
    splash.append(b.group)

You can add more buttons by defining a new Button() object with a new name and adding it to the buttons group like this:

button_3 = Button(x=BUTTON_MARGIN, y=BUTTON_MARGIN*3+BUTTON_HEIGHT*2, width=BUTTON_WIDTH, height=BUTTON_HEIGHT, label="Button 3", label_font=font, style=Button.SHADOWROUNDRECT, label_color=0x505050, fill_color=0x9e9e9e, outline_color=0x464646)

<>buttons.append(button_3<span)

Though you may want to change the BUTTON_HEIGHT so that this third button will fit under the other two buttons.

Label text

Now we want to set up the text areas that will be used to display sensor data along with feed data from Home Assistant.

Just like the buttons, we need to create a unique Label object using the Label( ) function.

light_label = Label(font, text="lux", color=0xE300D2, max_glyphs=40)

This function needs to know the following:

  • font - what font are you using for this label?
  • text - the text that you want to populate this label with
  • color - what color do you want this text to be?
  • max_glyphs - this sets the maximum number of text characters that will be allowed into this label, but it is only important if the text will be changed later or is otherwise dynamic.

We will also set the x and y position of this label after it is created.

light_label.x = 130

light_label.y = 40

Next we add the new label to the splash group.

splash.append(light_label)

Download: file
# Text Label Objects
temperature_label = Label(font, text="temperature", color=0xE300D2, max_glyphs=40)
temperature_label.x = 130
temperature_label.y = 20
splash.append(temperature_label)

light_label = Label(font, text="lux", color=0xE300D2, max_glyphs=40)
light_label.x = 130
light_label.y = 40
splash.append(light_label)

motion_label = Label(font, text="motion", color=0xE300D2, max_glyphs=40)
motion_label.x = 130
motion_label.y = 60
splash.append(motion_label)

feed1_label = Label(font, text="MQTT feed1", color=0xE39300, max_glyphs=100)
feed1_label.x = 130
feed1_label.y = 130
splash.append(feed1_label)

feed2_label = Label(font, text="MQTT feed2", color=0x00DCE3, max_glyphs=100)
feed2_label.x = 130
feed2_label.y = 200
splash.append(feed2_label)

Again, you can add more Label objects if you like. Just change the x and y numbers to ensure that everything fits nicely.

Setting the MQTT Topics

This is a list of all feeds that your PyPortal will interact with. To keep things organized we are starting with the group feed pyportal.

Download: file
# ------------- MQTT Topic Setup ------------- #

mqtt_topic = 'test/topic'
mqtt_temperature = 'pyportal/temperature'
mqtt_lux = 'pyportal/lux'
mqtt_PIR = 'pyportal/pir'
mqtt_button1 = 'pyportal/button1'
mqtt_button2 = 'pyportal/button2'
mqtt_feed1 = 'pyportal/feed1'
mqtt_feed2 = 'pyportal/feed2'

You can always add more feeds if you want.

The nice thing about MQTT is that a client device like this one can create a new feed simply by requesting one.

MQTT Functions

This is where we set up to do all of the connection and interactions with the MQTT server. Most of these functions should be kept the same unless you want to do a specific thing when say your PyPortal has connected to the MQTT server.

The important part of this section is the message function that handles incoming MQTT data from feeds that you are subscribed to.

When new data is posted to a feed, that data is sent to the PyPortal if it is a feed that this device has subscribed to. The message function captures the feed topic and message so that it can be passed to the code loop or otherwise acted on within the function.

This example looks to see if the message is from one of the topics that we want to display.

Next if the topic is pyportal/feed1 or pyportal/feed2, it will format the message and set the text for the appropriate Label to the data from the revived message.

If the topic is for pyportal/button1, the message is filtered and used to set the button1 , otherwise known as buttons[0], state to match the new data. This is used to show how you would link switch type objects so that they all represents the current state of that MQTT feed.

In other words, the switch on the PyPortal will always be in the same state as the switch in Home Assistant so long as they both get data from the same feed.

It is not recommended to use a large volume of code in the message function, as it is run frequently. Try to add the minimum code needed to process your incoming message, and try not to run code here that is not related to a subscribed feed.
Download: file
def message(client, topic, message):
    """Method callled when a client's subscribed feed has a new
    value.
    :param str topic: The topic of the feed with a new value.
    :param str message: The new value
    """
    print('New message on topic {0}: {1}'.format(topic, message))
    if topic == "pyportal/feed1":
        feed1_label.text = 'Next Bus: {}'.format(message)
    if topic == "pyportal/feed2":
        feed2_label.text = 'Weather: \n    {}'.format(message)
    if topic == "pyportal/button1":
        if message == "1":
            buttons[0].label="ON"
            buttons[0].selected = False
            print("Button 1 ON")
        else:
            buttons[0].label="OFF"
            buttons[0].selected = True
            print("Button 1 OFF")

If you are subscribing to more feeds, you will want to add code here to process that message with the following code where MyMessageTopic represents the feed topic that you have subscribed to and newFeed_label represents a label object.

Download: file
if topic == "MyMessageTopic":
  newFeed_label.text = 'New Feed: \n {}'.format(message)

Subscribing to the feeds

Now we skip over the network connection handling and get to where we actually tell our MQTT server what topics we would like to subscribe to.

Basically, if you want the PyPortal to be updated with any information from the MQTT server, you will need to subscribe to that topic.

Download: file
print('Subscribing to %s, %s, and %s' % (mqtt_feed1, mqtt_feed2, mqtt_button1))
client.subscribe(mqtt_feed1)
client.subscribe(mqtt_feed2)
client.subscribe(mqtt_button1)

The Loop

Now we are into the code loop and the first thing we want to run is client.loop() witch checks for new MQTT message updates.

Next we are going to read some sensors, assign their values to a variable, and update the relevant label text by running:

light_label.text = 'Light Sensor: {}'.format(light_value)

for each of the inputs except for the display buttons.

Download: file
# ------------- Code Loop ------------- #
while True:
    # Poll the message queue
    client.loop()

    # Read sensor data and format
    light_value = lux = light_sensor.value
    light_label.text = 'Light Sensor: {}'.format(light_value)
    temperature = round(adt.temperature)
    temperature_label.text = 'Temp Sensor: {}'.format(temperature)
    movement_value = movement_sensor.value
    motion_label.text = 'PIR Sensor: {}'.format(movement_value)

The button handler

Here is where we decide what happens when the onscreen buttons are pressed. This code will only be run if the screen is touched based on whether b.contains(touch) or not.

Button 1 is tested first using if i=0: because Button 1 is the first button in the button group array.

Then if button1_state == 0 that means that it was off when the button was pressed, so we will now switch the button1_state to 1 so that it is ON. The opposite is done if button1_state started with a value of 1 since the test statement is FALSE. This is a simple way to make a toggle state button. We are also using b.selected = True/False to change the look of the button when toggled. Last thing for Button 1 is to use client.publish(mqtt_button1, button1_state) to publish the new state of Button 1 and then we use while ts.touch_point: as a debounce so that nothing happens until the button is released.

Button 2 is tested first using if i=1: because Button 2 is the second button in the button group array.

This is a more simple button and it will just use client.publish(mqtt_button2, 1) to publish the Pressed state of the button. It will then wait for the button to be released before it resets Button 2 and calls client.publish(mqtt_button2, 0) to publish the Not Pressed state of the button. This will allow us to create Automations later for short and long pressing of this button.

Download: file
# Read display button press
    touch = ts.touch_point
    if touch:
        for i, b in enumerate(buttons):
            if b.contains(touch):
                print('Sending button%d pressed' % i)
                if i == 0:
                    # Toggle switch button type
                    if button1_state == 0:
                        button1_state = 1
                        b.label = "ON"
                        b.selected = False
                        print("Button 1 ON")
                    else:
                        button1_state = 0
                        b.label = "OFF"
                        b.selected = True
                        print("Button 1 OFF")
                    print('Sending button 1 state: ')
                    client.publish(mqtt_button1, button1_state)
                    # for debounce
                    while ts.touch_point:
                        print("Button 1 Pressed")
                if i == 1:
                    # Momentary button type
                    b.selected = True
                    print('Sending button 2 state: ')
                    client.publish(mqtt_button2, 1)
                    # for debounce
                    while ts.touch_point:
                        print("Button 2 Pressed")
                    print("Button 2 reliced")
                    print('Sending button 2 state: ')
                    client.publish(mqtt_button2, 0)
                    b.selected = False

Publishing the Sensors

Now we get to the last bit where we simply publish the values of each sensor to it's relevant MQTT topic. This is done by use of the client.publish() function which needs the following parameters:

  • MQTT Topic to publish to
  • The message to publish in string format
Download: file
# Publish sensor data to MQTT
    print('Sending light sensor value: %d' % light_value)
    client.publish(mqtt_lux, light_value)

    print('Sending temperature value: %d' % temperature)
    client.publish(mqtt_temperature, temperature)

    print('Sending motion sensor value: %d' % movement_value)
    client.publish(mqtt_PIR, '{}'.format(movement_value))

And that is the end of our code. If you need more help with getting this code to work, have a look at the following guides that were used to create this code.

This guide was first published on Jan 08, 2020. It was last updated on Jan 08, 2020.
This page (Code Breakdown) was last updated on Aug 12, 2020.