You've gotten the code up and running. Excellent! However, you may be wondering exactly what's going on with the code. This page will walk you through the code, section by section, and explain what's happening.

Set Up Code

First, you'll go through the sections of code related to set up, found at the beginning of the file.

Imports and Configuration

The code begins with importing the necessary libraries and modules.

import json
import time
import serial
import requests
import os

Imports are followed by the configuration section explained in the previous page of this guide. You should have already configured this section to meet your needs and your choice of projects.

REPO_WORKFLOW_URL = "https://api.github.com/repos/adafruit/circuitpython/actions/workflows/build.yml/runs"
POLL_DELAY = 60 * 3
COMPLETION_LIGHT_TIME = 30
COMPLETION_BUZZER_TIME = 1
ENABLE_USB_LIGHT_MESSAGES = True

# serial_port = "COM57"
serial_port = "/dev/tty.usbserial-144430"

Tower Light Constants and Baud Rate

The USB tower light is controlled over a serial connection from your computer. To turn on the lights and make sound with the buzzer, you need to send a command using a hex value constant. Assigning these constants to variables makes it much easier to identify which value does what, and makes the code significantly more readable. Therefore, the code includes a list of those constants and their variables.

RED_ON = 0x11
RED_OFF = 0x21
RED_BLINK = 0x41

YELLOW_ON = 0x12
YELLOW_OFF = 0x22
YELLOW_BLINK = 0x42

GREEN_ON = 0x14
GREEN_OFF = 0x24
GREEN_BLINK = 0x44

BUZZER_ON = 0x18
BUZZER_OFF = 0x28
BUZZER_BLINK = 0x48

Serial communication requires you to set a baud rate, which is the speed of the serial communication. This is done here.

baud_rate = 9600

Convenience Functions

There are three functions utilised in this example.

The first function enables you to send a command. It requires you to provide a serialport (defined later in this example) and a command.

def send_command(serialport, cmd):
    serialport.write(bytes([cmd]))

The second function turns off all of the lights and the buzzer, allowing you to reset the state of the tower light where needed.

def reset_state():
    # Clean up any old state
    send_command(mSerial, BUZZER_OFF)
    send_command(mSerial, RED_OFF)
    send_command(mSerial, YELLOW_OFF)
    send_command(mSerial, GREEN_OFF)

The third function includes code to turn the buzzer on for a period of time defined in the configuration section, if the buzzer is not disabled (e.g. COMPLETION_BUZZER_TIME set to 0 in configuration). This function is included simply to avoid duplicate code where the buzzer is used.

def buzzer_on_completion():
    if COMPLETION_BUZZER_TIME > 0:
        send_command(mSerial, BUZZER_ON)
        time.sleep(COMPLETION_BUZZER_TIME)
        send_command(mSerial, BUZZER_OFF)

Workflow ID List

The code keeps track of completed workflows by adding the workflow ID number into a list, and querying that list so that it doesn't repeatedly notify you about completed workflows. You must create an empty list before you can add content to it.

already_shown_ids = []

HTTP Header for GitHub API

This code is required to be able to request data from the GitHub API. Your GitHub API Token should have been made available when you configured the environment variable. If you opted not to do this, you can paste it directly into the code, replacing GITHUB_API_TOKEN with your personal access token.

Including your personal access token directly in your code is not recommended. It is much safer to use environment variables.
headers = {'Accept': "application/vnd.github.v3+json",
           'Authorization': f"token {os.getenv('GITHUB_API_TOKEN')}"}

Serial Connection

As this code can be run without the hardware (if you set ENABLE_USB_LIGHT_MESSAGES = False in the configuration stage), you do not necessarily want to create the serial connection if you're not going to be using it. So, to ensure the code runs, the mSerial variable is initially created as None. Then, if the tower light communication is enabled, it initializes the serial connection as mSerial for use later.

mSerial = None
if ENABLE_USB_LIGHT_MESSAGES:
    print("Opening serial port.")
    mSerial = serial.Serial(serial_port, baud_rate)

Starting Status Watcher

Finally, immediately before the main loop, two lines are printed to the serial console, informing you of the status watcher starting, and how to exit the program when you desire. The code will run in an infinite loop until you interrupt it by pressing CTRL+C on your keyboard.

print("Starting Github Actions Status Watcher")
print("Press Ctrl-C to Exit")

Main Loop

This section walks through all the code found in the main loop in this example.

Immediately before the main loop begins, you'll find a try.

try:
    while True:

This is part of a try/except block surrounding the entire main loop. It will be explained in full at the end of this page.

Fetch Workflow Data

Before the fetch begins, "Fetching workflow run status." is printed to the serial console, to let you know what comes next.

Then, you perform a GET request to the GitHub API to pull the current Actions workflow data.

This data can be returned in multiple ways. This example will use JSON, so following the request for current data, we access the response as JSON data.

Next, you open a file named actions_status_result.json to which to write the results, write the results to the newly opened file, and then close the file to complete the file write.

[...]
        print("Fetching workflow run status.")
        response = requests.get(f"{REPO_WORKFLOW_URL}?per_page=1", headers=headers)
        response_json = response.json()
        f = open("action_status_result.json", "w")
        f.write(json.dumps(response_json))
        f.close()

Actions Workflow ID Lookup

The next step is to look up the ID number for the current Actions workflow from the JSON data, and save it to a variable. This is necessary to keep track of workflow IDs later in the code.

[...]
    	workflow_run_id = response_json['workflow_runs'][0]['id']

Workflow Status and Conclusion

Later in the code, if an Actions run is completed, the ID is added to the already_shown_ids list that was created towards the beginning. This is where that list is checked, to determine whether the latest Actions workflow ID is in that list.

If the current workflow ID is not on the already shown list, the program continues on to the rest of the code nested beneath.

The code then looks up the current workflow's status and conclusion. The status can be queued, in progress or completed. The conclusion applies only to the completed status, and can be either success or failure. (Conclusion is None for queued and in progress.)

It then outputs the status and conclusion by printing to the serial console.

[...]
        if workflow_run_id not in already_shown_ids:
            status = response_json['workflow_runs'][0]['status']
            conclusion = response_json['workflow_runs'][0]['conclusion']
            print(f"Status - Conclusion: {status} - {conclusion}")

Status: Queued

If the status is queued, it prints the active status to the serial console. If the tower light is enabled, it prints the serial command being sent, and sends the serial command YELLOW_BLINK. This makes the tower light blink yellow. The yellow blinking will continue as long as the workflow is queued.

[...]
            if status == "queued":
                print("Actions run status: Queued.")
                if ENABLE_USB_LIGHT_MESSAGES:
                    print("Sending serial command 'YELLOW_BLINK'.")
                    send_command(mSerial, YELLOW_BLINK)

Status: In Progress

If the status is in progress, it prints the active status to the serial console. If the tower light is enabled, it prints the serial command being sent, and sends the serial command YELLOW. This makes the light turn yellow. The yellow light will continue as long as the workflow is in progress.

[...]
            if status == "in_progress":
                print("Actions run status: In progress.")
                if ENABLE_USB_LIGHT_MESSAGES:
                    print("Sending serial command 'YELLOW_ON'.")
                    send_command(mSerial, YELLOW_ON)

Status: Completed

If the status is completed, the first thing that happens is printing to the serial console that the workflow ID is being added to the already shown IDs list, and the workflow ID number is added to the list.

[...]
            if status == "completed":
                print(f"Adding {workflow_run_id} to shown workflow IDs.")
                already_shown_ids.append(workflow_run_id)

Conclusion: Success

If the completed status conclusion is success, it prints this info to the serial console. If the tower light is enabled, it first sends the command YELLOW_OFF, in the event that the yellow light is still on. Next, it prints to the serial console the next command being sent, and sends the command GREEN_ON.

Following that, it runs the buzzer_on_completion() function to turn the buzzer on for the duration you previously configured.

Then the code pauses for the duration of the buzzer subtracted from the duration you configured the light to remain on. This ensures that the light will remain on as long as you configured it for, regardless of how long you turn on the buzzer.

Finally, when the completion light time duration is up, it prints to the serial console the command it's sending, and sends the command GREEN_OFF.

[...]
                if conclusion == "success":
                    print("Actions run status: Completed - successful.")
                    if ENABLE_USB_LIGHT_MESSAGES:
                        send_command(mSerial, YELLOW_OFF)
                        print("Sending serial command 'GREEN_ON'.")
                        send_command(mSerial, GREEN_ON)
                        buzzer_on_completion()
                    time.sleep(COMPLETION_LIGHT_TIME - COMPLETION_BUZZER_TIME)
                    if ENABLE_USB_LIGHT_MESSAGES:
                        print("Sending serial command 'GREEN_OFF'.")
                        send_command(mSerial, GREEN_OFF)

Conclusion: Failure

If the completed status conclusion is failure, it follows the same set of steps as the success conclusion. The differences are the info printed to the serial console, and the commands being sent to the light. The printed info indicates the failure conclusion. The commands sent to the light are RED_ON, and the configured amount of time later, RED_OFF.

[...]
                if conclusion == "failure":
                    print("Actions run status: Completed - failed.")
                    if ENABLE_USB_LIGHT_MESSAGES:
                        send_command(mSerial, YELLOW_OFF)
                        print("Sending serial command 'RED_ON'.")
                        send_command(mSerial, RED_ON)
                        buzzer_on_completion()
                    time.sleep(COMPLETION_LIGHT_TIME - COMPLETION_BUZZER_TIME)
                    if ENABLE_USB_LIGHT_MESSAGES:
                        print("Sending serial command 'RED_OFF'.")
                        send_command(mSerial, RED_OFF)

Conclusion: Cancelled

If the completed status conclusion is failure, it follows the same set of steps as the success and failure conclusions. The differences are the info printed to the serial console, and the commands being sent to the light. The printed info indicates the cancelled conclusion. The commands sent to the light are RED_BLINK, and the configured amount of time later, RED_OFF.

[...]
                if conclusion == "cancelled":
                    print("Actions run status: Completed - cancelled.")
                    if ENABLE_USB_LIGHT_MESSAGES:
                        send_command(mSerial, YELLOW_OFF)
                        print("Sending serial command 'RED_BLINK'.")
                        send_command(mSerial, RED_BLINK)
                        buzzer_on_completion()
                    time.sleep(COMPLETION_LIGHT_TIME - COMPLETION_BUZZER_TIME)
                    if ENABLE_USB_LIGHT_MESSAGES:
                        print("Sending serial command 'RED_OFF'.")
                        send_command(mSerial, RED_OFF)

Already Followed and Query Delay

The code ends with an else block that is matched up with the if workflow_run_id not in already_shown_ids: line from the Workflow Status and Conclusion section above. If the workflow_run_id is in the already_shown_ids list, then the code in the else block is run. It simply prints to the serial console, "Already followed the current run." to let you know that no new Actions run has occurred since the completion of the previous one.

Then, no matter what, the code sleeps for the POLL_DELAY duration. This is the length of time between queries to the GitHub API, configured in the Configuration section of this guide.

[...]
        else:
            print("Already followed the current run.")
        time.sleep(POLL_DELAY)

try / except Block

As mentioned at the beginning of the Main Loop section, the entire main loop is wrapped in a try / except. If you recall, you must press CTRL+C to exit the program. This causes a KeyboardInterrupt, which can be caught by an except, enabling you to run code when exiting the program.

This block tries to run the main loop, and if a KeyboardInterrupt is detected, it instead runs the code within the except. This code prints the exiting status to the serial console, and runs the reset_state() function to turn off all the possible tower light actions (i.e. the LEDs and the buzzer).

try:
[...]  # Main loop.
except KeyboardInterrupt:
    print("\nExiting Github Actions Status Watcher.")
    reset_state()

In this case, the problem was that without this block, if you exited the program while the tower light was doing something (e.g. the yellow light blinking, or green light and buzzer going off), the light would continue its current state after the program had exited. To terminate the continued tower light state, you would be required to unplug the light from your computer (to power it off) and plug it back in to be ready for the next time you run the code.

This solves that problem by using the reset_state() function to send the *_OFF commands for all possible light functionality. When you use CTRL+C to exit the program, the tower light will stop any current actions as well.

This guide was first published on Jul 06, 2022. It was last updated on 2022-07-06 09:21:47 -0400.

This page (Code Walkthrough) was last updated on Sep 25, 2022.

Text editor powered by tinymce.