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.
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.
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.
Text editor powered by tinymce.