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.
# ------- 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.
# ---------- 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.root_group = 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].
# ------------- User Inretface Eliments ------------- # # Make the display context splash = displayio.Group() board.DISPLAY.root_group = 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.
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)
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?
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)
# Text Label Objects temperature_label = Label(font, text="temperature", color=0xE300D2) temperature_label.x = 130 temperature_label.y = 20 splash.append(temperature_label) light_label = Label(font, text="lux", color=0xE300D2) light_label.x = 130 light_label.y = 40 splash.append(light_label) motion_label = Label(font, text="motion", color=0xE300D2) motion_label.x = 130 motion_label.y = 60 splash.append(motion_label) feed1_label = Label(font, text="MQTT feed1", color=0xE39300) feed1_label.x = 130 feed1_label.y = 130 splash.append(feed1_label) feed2_label = Label(font, text="MQTT feed2", color=0x00DCE3) 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
.
# ------------- 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.
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.
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.
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.
# ------------- 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.
# 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
# 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.
Page last edited March 08, 2024
Text editor powered by tinymce.