Maureen Rakotondraibe
Published © GPL3+

Bus o'clock - Visual bus tracker (Chicago)

Visual bus tracker (Chicago) using a Raspberry Pi Zero 2W and a round display.

IntermediateFull instructions provided6 hours226
Bus o'clock - Visual bus tracker (Chicago)

Things used in this project

Hardware components

Raspberry Pi Zero 2 W
Raspberry Pi Zero 2 W
×1
5inch HDMI Round Touch Display, 1080 × 1080
×1

Story

Read more

Custom parts and enclosures

User interface background

Image used as the background designed using Figma.

Schematics

Wiring diagram

The picture shows the wiring diagram between a RPI Zero 2W and the round display. More setups with other RPIs are available in the display wiki page.

Code

bus_tracker.py

Python
This code pulls the data from the CTA server and check if the needle should be in the no-bus zone.
import requests
import datetime
import time
key = 'insert-your-key'
bus_number = 0
def get_next_bus(bus_number,position):
    params = {'format' : 'json', 'key' : key}
    x = requests.get('http://www.ctabustracker.com/bustime/api/v3/gettime',params)
    time = x.json()['bustime-response']['tm']
    time = datetime.datetime.strptime(time,'%Y%m%d %H:%M:%S')
    #print(time)

    stpid = 'your-bus-stip-id"
    rt = 'your-bus-route-number'
    
    if bus_number == 0:
        params = {'format' : 'json', 'key' : key, 'stpid': stpid, 'rt': rt}
        x = requests.get('http://www.ctabustracker.com/bustime/api/v3/getpredictions', params)
    x = x.json()
    #print(x)
    try:
        bus_time = x['bustime-response']['prd'][position]['prdtm']
        bus_time = datetime.datetime.strptime(bus_time,'%Y%m%d %H:%M')
        next_bus = (bus_time-time).total_seconds()/60
        bus_nb = x['bustime-response']['prd'][position]['vid'] 
        return next_bus,bus_nb
    except: #No bus incoming
        error = x['bustime-response']['error'][0]['msg']
        print(error)
        return -1,0

def bus_reset():
    next_bus,bus_nb = get_next_bus(bus_number,0)
    print(next_bus,bus_nb)
    if next_bus == -1: #no-bus zone
        return 30
    elif next_bus < 0: 
        return 0
    elif nex_bus > 20: #on-hold zone
        return 22
    return next_bus

bus_tracker_ui.py

Python
This code draws the user interface and run the main program. It calls functions in the bus_tracker,py file to fetch data and updates the user interface.
import tkinter as tk
from PIL import Image, ImageTk
import bus_tracker

target_angle = 0

# Function to update the rotation angle
def rotate_needle(canva, image_item, angle,target):
    rotated_image = ImageTk.PhotoImage(fg_img.rotate(angle))
    canva.itemconfig(image_item, image=rotated_image)
    canva.image = rotated_image  # Store the reference to prevent garbage collection 
    if (angle) != target:
        angle = (angle - 5) % 360
        canva.after(50, lambda: rotate_needle(canvas, image_item, angle,target))
    else:
        print('New position')

# Fetch new data and update the gauge
def get_bus(angle=90):
    global target_angle, next_bus
    minutes = bus_tracker.bus_reset()
    target_angle = (minutes*9 +270)%360
    target_angle =  int(round(target_angle/5.0)*5.0)
    # Start the rotation animation
    rotate_needle(canvas, needle_item, angle,target_angle)
    # Get the new time remaining in 1 minute
    next_bus = root.after(10000, lambda:get_bus(target_angle))

#Stop the bus requests and put the needle in the on-hold zone
def stop_bus():
    global target_angle, next_bus
    reset_angle = (22*9 +270)%360
    reset_angle =  int(round(reset_angle/5.0)*5.0)
    print(reset_angle)
    print(target_angle)
    rotate_needle(canvas, needle_item, target_angle,reset_angle)
    target_angle = reset_angle
    try:
        root.after_cancel(next_bus)
    except:
        pass

#Window UI
width = 1080
height = 1080
root = tk.Tk()
root.geometry(str(width) + "x" +str(height))
root.overrideredirect(True)
root.config(cursor="none")

frame = tk.Frame(root)
frame.grid()

canvas = tk.Canvas(frame, width=width, height=height)
canvas.grid()

#UI setup
##Gauge image 
background = tk.PhotoImage(file="gauge2big.png")
canvas.create_image(0,5, anchor= tk.NW,image=background)

##Needle image
fg_img = Image.open("needle.gif").convert('RGBA')
needle = ImageTk.PhotoImage(fg_img)
needle_item = canvas.create_image(width/2,height/2+130, anchor= tk.CENTER,image=needle)

##Refresh button
refresh_img= tk.PhotoImage(file='refresh2.png')
refresh_btn= tk.Button(frame, image=refresh_img,command= lambda:get_bus(target_angle),
highlightthickness=0, bd=0, borderwidth=0,pady=0, padx=0,width=80, height=65)
canvas.create_window(int(width*3/4)-82+90, int(height*2/3)+125, window=refresh_btn)

##Stop button
stop_img= tk.PhotoImage(file='pause2.png')
stop_btn= tk.Button(frame, image=stop_img,command= stop_bus,
highlightthickness=0, bd=0, borderwidth=0,pady=0, padx=0,width=95, height=65)
canvas.create_window(int(width*3/4)-82, int(height*2/3)+125, window=stop_btn)

#initialize in the on-hold zone
stop_bus()

root.mainloop()

Credits

Maureen Rakotondraibe
5 projects • 7 followers
Embedded systems/software engineer / Passionate about interfacing technologies to create new solutions

Comments