Files
CTA_ESP32/examples/src/micropython-webserver/main.py
T
2026-10-05 18:11:21 +10:00

449 lines
22 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from time import time, gmtime, mktime,sleep
import network, machine, ntptime, sockets, json, requests
def time_from_iso(datetime_str):
[yyyy, mm, dd] = [int(i) for i in datetime_str.split('T')[0].split('-')]
[h,m,s] = [int(i) for i in datetime_str.split('T')[1].split(':')]
time_u = mktime((yyyy, mm, dd, h, m, s, 0, 0))
return time_u
def start_ap():
# Sets up initial captive portal for configuring wifi and basic options
ap = network.WLAN(network.AP_IF)
ap.active(true)
ap.config(
essid="CTA-Setup",
authmode=network.AUTH_OPEN
)
print("AP active for initial setup: ", ap.active())
print("AP IP address:", ap.ifconfig()[0])
return ap
def parse_form_data(body):
"""Parse URL-encoded form data"""
data = {}
pairs = body.split("&")
for pair in pairs:
if "=" in pair:
key, value = pair.split("=", 1)
# URL decode
data[key] = value.replace("+", " ").replace("%40", "@")
return data
def save_config(data):
"""Save Wi-Fi credentials to config.json"""
if not data.get("SSID"):
raise ValueError("SSID is required")
config = {
"SSID": data.get("SSID"),
"PASS": data.get("PASS", ""),
"APIKEY": data.get("APIKEY", "")
}
with open("config.json", "w") as f:
json.dump(config, f)
print("Configuration saved:", config)
def start_http_server():
addr = socket.getaddrinfo("0.0.0.0", 80)[0][-1]
s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(addr)
s.listen(1)
print("HTTP server listening on port 80")
while True:
try:
conn, addr = s.accept()
request = conn.recv(2048).decode()
if request.startswith("POST /save"):
# Handle form submission
body = request.split("\r\n\r\n", 1)[1]
form_data = parse_form_data(body)
save_config(form_data)
response_body = """<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Setup Complete</title>
<style>
body { font-family: sans-serif; padding: 16px; text-align: center; }
h2 { color: #28a745; }
</style>
</head>
<body>
<h2>Configuration saved</h2>
<p>Device will reboot and connect to Wi-Fi.</p>
<p><small>You can close this window.</small></p>
</body>
</html>"""
else:
# Serve the setup page for any other request
# Build list of stations
html_list = ''
for station, dict in stations.items(): # loop through all stations
html_list += f'<option value={station}>{stations[station]["name"]}</option>'
response_body = """<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Device Setup</title>
<style>
body {
font-family: sans-serif;
padding: 16px;
max-width: 400px;
margin: 0 auto;
}
h2 {
margin-top: 0;
}
label {
display: block;
margin-top: 12px;
font-weight: bold;
}
input, button {
width: 100%;
padding: 10px;
margin-top: 6px;
font-size: 16px;
box-sizing: border-box;
}
button {
margin-top: 20px;
background: #007bff;
color: white;
border: none;
cursor: pointer;
}
button:hover {
background: #0056b3;
}
</style>
</head>
<body>
<h2>Device Setup</h2>
<form method="POST" action="/save">
<label>
Wi-Fi SSID
<input name="SSID" required>
</label>
<label>
Password
<input name="PASS" type="password">
</label>
<label>
CTA API Key
<input name="APIKEY">
</label>
<label>
Left LED Station:
<select name="Station">
{}
</select>
</label>
<button type="submit">Save & Connect</button>
</form>
</body>
</html>""".format(html_list)
response = (
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html\r\n"
"Connection: close\r\n\r\n"
+ response_body
)
conn.send(response.encode())
except Exception as e:
print("HTTP error:", e)
try:
error = f"<h2>Error</h2><p>{e}</p>"
conn.send(
"HTTP/1.1 500 Internal Server Error\r\n"
"Content-Type: text/html\r\n"
"Connection: close\r\n\r\n"
.encode() + error.encode()
)
except:
pass
finally:
try:
conn.close()
except:
pass
def reconnect_wifi():
while not wlan.isconnected():
try:
wlan.connect(SSID, PASS)
max_wait = 10
while max_wait > 0:
if wlan.isconnected():
break
max_wait -= 1
print('Waiting for connection...')
sleep(1)
if wlan.isconnected():
print('Connected to Wi-Fi')
print('Network config:', wlan.ifconfig())
break
except Exception as e:
print('Connection error:', e)
print('Retrying connection...')
sleep(2)
def init_LEDs():
# Sets up LEDs and their ports.
led = machine.Pin(15, machine.Pin.OUT)
led.off()
# Synchronize time of RTC with NTP
last_ntp_sync = -3700 # Init as more than an hour ago
last_api_query = mktime([2000,1,1,0,0,0,0,0]) # Set last query default (will run on first run)
query_period = 120 # Query the API at most every 2 minutes
distance_on = 10 # Time from station before turning on LED
# Stations dictionary contains name, the ETA of all trains as an array, the LED index of the light and the LED state
stations = {}
stations[41090] = { 'name':' Monroe (Red Line)', 'eta':[], 'led_index':1, 'state':0 }
stations[40820] = { 'name':' Rosemont (Blue Line)', 'eta':[], 'led_index': machine.Pin(15, machine.Pin.OUT), 'state':0 }
stations[41010] = { 'name':' Rockwell (Brown Line)', 'eta':[], 'led_index':3, 'state':0 }
stations[40470] = { 'name':' Racine (Blue Line)', 'eta':[], 'led_index':4, 'state':0 }
stations[40190] = { 'name':' Sox-35th (Red Line)', 'eta':[], 'led_index':5, 'state':0 }
stations[40020] = { 'name':' Harlem/Lake (Green Line)', 'eta':[], 'led_index':6, 'state':0 }
stations[40900] = { 'name':' Howard (Red Purple & Yellow lines)', 'eta':[], 'led_index':7, 'state':0 }
stations[41160] = { 'name':' Clinton (Green & Pink lines)', 'eta':[], 'led_index':8, 'state':0 }
stations[40230] = { 'name':' Cumberland (Blue Line)', 'eta':[], 'led_index':9, 'state':0 }
stations[40560] = { 'name':' Jackson (Red Line)', 'eta':[], 'led_index':10, 'state':0 }
stations[41150] = { 'name':' Kedzie (Orange Line)', 'eta':[], 'led_index':11, 'state':0 }
stations[40780] = { 'name':' Central Park (Pink Line)', 'eta':[], 'led_index':12, 'state':0 }
stations[41080] = { 'name':' 47th (Green Line)', 'eta':[], 'led_index':13, 'state':0 }
stations[40510] = { 'name':' Garfield (Green Line)', 'eta':[], 'led_index':14, 'state':0 }
stations[41050] = { 'name':' Linden (Purple Line)', 'eta':[], 'led_index':2, 'state':0 }
stations[40120] = { 'name':' 35th/Archer (Orange Line)', 'eta':[], 'led_index':16, 'state':0 }
stations[40570] = { 'name':' California (Blue Line)', 'eta':[], 'led_index':17, 'state':0 }
stations[40260] = { 'name':' State/Lake (Brown Green Orange Pink & Purple lines)', 'eta':[], 'led_index':18, 'state':0 }
stations[40130] = { 'name':' 51st (Green Line)', 'eta':[], 'led_index':19, 'state':0 }
stations[41700] = { 'name':' Washington/Wabash (Brown Green Orange Purple & Pink lines)', 'eta':[], 'led_index':20, 'state':0 }
stations[41000] = { 'name':' Cermak-Chinatown (Red Line)', 'eta':[], 'led_index':21, 'state':0 }
stations[40340] = { 'name':' Berwyn (Red Line)', 'eta':[], 'led_index':22, 'state':0 }
stations[41690] = { 'name':' Cermak-McCormick Place (Green Line)', 'eta':[], 'led_index':23, 'state':0 }
stations[40490] = { 'name':' Grand (Blue Line)', 'eta':[], 'led_index':24, 'state':0 }
stations[41060] = { 'name':' Ashland (Orange Line)', 'eta':[], 'led_index':25, 'state':0 }
stations[40090] = { 'name':' Damen (Brown Line)', 'eta':[], 'led_index':26, 'state':0 }
stations[41280] = { 'name':' Jefferson Park (Blue Line)', 'eta':[], 'led_index':27, 'state':0 }
stations[40520] = { 'name':' Foster (Purple Line)', 'eta':[], 'led_index':28, 'state':0 }
stations[40330] = { 'name':' Grand (Red Line)', 'eta':[], 'led_index':29, 'state':0 }
stations[40280] = { 'name':' Central (Green Line)', 'eta':[], 'led_index':30, 'state':0 }
stations[40250] = { 'name':' Kedzie-Homan (Blue Line)', 'eta':[], 'led_index':31, 'state':0 }
stations[40870] = { 'name':' Francisco (Brown Line)', 'eta':[], 'led_index':32, 'state':0 }
stations[41240] = { 'name':' Addison (Blue Line)', 'eta':[], 'led_index':33, 'state':0 }
stations[40740] = { 'name':' Western (Pink Line)', 'eta':[], 'led_index':34, 'state':0 }
stations[41360] = { 'name':' California (Green Line)', 'eta':[], 'led_index':35, 'state':0 }
stations[41400] = { 'name':' Roosevelt (Red Orange & Green lines)', 'eta':[], 'led_index':36, 'state':0 }
stations[41410] = { 'name':' Chicago (Blue Line)', 'eta':[], 'led_index':37, 'state':0 }
stations[40680] = { 'name':' Adams/Wabash (Brown Green Orange Pink & Purple lines)', 'eta':[], 'led_index':38, 'state':0 }
stations[40920] = { 'name':' Pulaski (Blue Line)', 'eta':[], 'led_index':39, 'state':0 }
stations[41210] = { 'name':' Wellington (Brown & Purple lines)', 'eta':[], 'led_index':40, 'state':0 }
stations[41340] = { 'name':' LaSalle (Blue Line)', 'eta':[], 'led_index':41, 'state':0 }
stations[41490] = { 'name':' Harrison (Red Line)', 'eta':[], 'led_index':42, 'state':0 }
stations[41350] = { 'name':' Oak Park (Green Line)', 'eta':[], 'led_index':43, 'state':0 }
stations[40040] = { 'name':' Quincy/Wells (Brown Orange Purple & Pink lines)', 'eta':[], 'led_index':44, 'state':0 }
stations[40390] = { 'name':' Forest Park (Blue Line)', 'eta':[], 'led_index':45, 'state':0 }
stations[41130] = { 'name':' Halsted (Orange Line)', 'eta':[], 'led_index':46, 'state':0 }
stations[40960] = { 'name':' Pulaski (Orange Line)', 'eta':[], 'led_index':47, 'state':0 }
stations[40830] = { 'name':' 18th (Pink Line)', 'eta':[], 'led_index':48, 'state':0 }
stations[40140] = { 'name':' Dempster-Skokie  (Yellow Line)', 'eta':[], 'led_index':49, 'state':0 }
stations[41320] = { 'name':' Belmont (Red Brown & Purple lines)', 'eta':[], 'led_index':50, 'state':0 }
stations[41660] = { 'name':' Lake (Red Line)', 'eta':[], 'led_index':51, 'state':0 }
stations[40910] = { 'name':' 63rd (Red Line)', 'eta':[], 'led_index':52, 'state':0 }
stations[40690] = { 'name':' Dempster (Purple Line)', 'eta':[], 'led_index':53, 'state':0 }
stations[40460] = { 'name':' Merchandise Mart (Brown & Purple lines)', 'eta':[], 'led_index':54, 'state':0 }
stations[40710] = { 'name':' Chicago (Brown & Purple lines)', 'eta':[], 'led_index':55, 'state':0 }
stations[40530] = { 'name':' Diversey (Brown & Purple lines)', 'eta':[], 'led_index':56, 'state':0 }
stations[40850] = { 'name':' Harold Washington Library-State/Van Buren (Brown Orange Purple & Pink lines)', 'eta':[], 'led_index':57, 'state':0 }
stations[41020] = { 'name':' Logan Square (Blue Line)', 'eta':[], 'led_index':58, 'state':0 }
stations[41030] = { 'name':' Polk (Pink Line)', 'eta':[], 'led_index':59, 'state':0 }
stations[41300] = { 'name':' Loyola (Red Line)', 'eta':[], 'led_index':60, 'state':0 }
stations[40310] = { 'name':' Western (Orange Line)', 'eta':[], 'led_index':61, 'state':0 }
stations[40430] = { 'name':' Clinton (Blue Line)', 'eta':[], 'led_index':62, 'state':0 }
stations[40320] = { 'name':' Division (Blue Line)', 'eta':[], 'led_index':63, 'state':0 }
stations[41510] = { 'name':' Morgan (Green & Pink lines)', 'eta':[], 'led_index':64, 'state':0 }
stations[40270] = { 'name':' Main (Purple Line)', 'eta':[], 'led_index':65, 'state':0 }
stations[41330] = { 'name':' Montrose (Blue Line)', 'eta':[], 'led_index':66, 'state':0 }
stations[40350] = { 'name':' UIC-Halsted (Blue Line)', 'eta':[], 'led_index':67, 'state':0 }
stations[40940] = { 'name':' Halsted (Green Line)', 'eta':[], 'led_index':68, 'state':0 }
stations[41480] = { 'name':' Western (Brown Line)', 'eta':[], 'led_index':69, 'state':0 }
stations[40030] = { 'name':' Pulaski (Green Line)', 'eta':[], 'led_index':70, 'state':0 }
stations[41190] = { 'name':' Jarvis (Red Line)', 'eta':[], 'led_index':71, 'state':0 }
stations[41170] = { 'name':' Garfield (Red Line)', 'eta':[], 'led_index':72, 'state':0 }
stations[40050] = { 'name':' Davis (Purple Line)', 'eta':[], 'led_index':73, 'state':0 }
stations[40750] = { 'name':' Harlem (Blue Line - OHare Branch)', 'eta':[], 'led_index':74, 'state':0 }
stations[41200] = { 'name':' Argyle (Red Line)', 'eta':[], 'led_index':75, 'state':0 }
stations[40440] = { 'name':' California (Pink Line)', 'eta':[], 'led_index':76, 'state':0 }
stations[40360] = { 'name':' Southport (Brown Line)', 'eta':[], 'led_index':77, 'state':0 }
stations[40760] = { 'name':' Granville (Red Line)', 'eta':[], 'led_index':78, 'state':0 }
stations[40480] = { 'name':' Cicero (Green Line)', 'eta':[], 'led_index':79, 'state':0 }
stations[40730] = { 'name':' Washington/Wells (Brown Orange Purple & Pink lines)', 'eta':[], 'led_index':80, 'state':0 }
stations[40580] = { 'name':' 54th/Cermak (Pink Line)', 'eta':[], 'led_index':81, 'state':0 }
stations[40650] = { 'name':' North/Clybourn (Red Line)', 'eta':[], 'led_index':82, 'state':0 }
stations[40220] = { 'name':' Western (Blue Line - Forest Park Branch)', 'eta':[], 'led_index':83, 'state':0 }
stations[40070] = { 'name':' Jackson (Blue Line)', 'eta':[], 'led_index':84, 'state':0 }
stations[41440] = { 'name':' Addison (Brown Line)', 'eta':[], 'led_index':85, 'state':0 }
stations[41670] = { 'name':' Conservatory (Green Line)', 'eta':[], 'led_index':86, 'state':0 }
stations[40790] = { 'name':' Monroe (Blue Line)', 'eta':[], 'led_index':87, 'state':0 }
stations[40180] = { 'name':' Oak Park (Blue Line)', 'eta':[], 'led_index':88, 'state':0 }
stations[40400] = { 'name':' Noyes (Purple Line)', 'eta':[], 'led_index':89, 'state':0 }
stations[41260] = { 'name':' Austin (Green Line)', 'eta':[], 'led_index':90, 'state':0 }
stations[40600] = { 'name':' Kostner (Pink Line)', 'eta':[], 'led_index':91, 'state':0 }
stations[40670] = { 'name':' Western (Blue Line - OHare Branch)', 'eta':[], 'led_index':92, 'state':0 }
stations[40100] = { 'name':' Morse (Red Line)', 'eta':[], 'led_index':93, 'state':0 }
stations[40770] = { 'name':' Lawrence (Red Line)', 'eta':[], 'led_index':94, 'state':0 }
stations[40150] = { 'name':' Pulaski (Pink Line)', 'eta':[], 'led_index':95, 'state':0 }
stations[40660] = { 'name':' Armitage (Brown & Purple lines)', 'eta':[], 'led_index':96, 'state':0 }
stations[40540] = { 'name':' Wilson (Red & Purple lines)', 'eta':[], 'led_index':97, 'state':0 }
stations[40380] = { 'name':' Clark/Lake (Blue Brown Green Orange Purple & Pink lines)', 'eta':[], 'led_index':98, 'state':0 }
stations[40160] = { 'name':' LaSalle/Van Buren (Brown Orange Purple & Pink lines)', 'eta':[], 'led_index':99, 'state':0 }
stations[40700] = { 'name':' Laramie (Green Line)', 'eta':[], 'led_index':100, 'state':0 }
stations[40240] = { 'name':' 79th (Red Line)', 'eta':[], 'led_index':101, 'state':0 }
stations[40290] = { 'name':' Ashland/63rd (Green Line)', 'eta':[], 'led_index':102, 'state':0 }
stations[40450] = { 'name':' 95th/Dan Ryan (Red Line)', 'eta':[], 'led_index':103, 'state':0 }
stations[40210] = { 'name':' Damen (Pink Line)', 'eta':[], 'led_index':104, 'state':0 }
stations[41140] = { 'name':' King Drive (Green Line)', 'eta':[], 'led_index':105, 'state':0 }
stations[41710] = { 'name':' Damen (Green Line)', 'eta':[], 'led_index':106, 'state':0 }
stations[40980] = { 'name':' Harlem (Blue Line - Forest Park Branch)', 'eta':[], 'led_index':107, 'state':0 }
stations[41270] = { 'name':' 43rd (Green Line)', 'eta':[], 'led_index':108, 'state':0 }
stations[40080] = { 'name':' Sheridan (Red Line)', 'eta':[], 'led_index':109, 'state':0 }
stations[40610] = { 'name':' Ridgeland (Green Line)', 'eta':[], 'led_index':110, 'state':0 }
stations[41070] = { 'name':' Kedzie (Green Line)', 'eta':[], 'led_index':111, 'state':0 }
stations[41430] = { 'name':' 87th (Red Line)', 'eta':[], 'led_index':112, 'state':0 }
stations[41250] = { 'name':' Central (Purple Line)', 'eta':[], 'led_index':113, 'state':0 }
stations[40880] = { 'name':' Thorndale (Red Line)', 'eta':[], 'led_index':114, 'state':0 }
stations[41680] = { 'name':' Oakton-Skokie (Yellow Line)', 'eta':[], 'led_index':115, 'state':0 }
stations[41120] = { 'name':' 35th-Bronzeville-IIT (Green Line)', 'eta':[], 'led_index':116, 'state':0 }
stations[41460] = { 'name':' Irving Park (Brown Line)', 'eta':[], 'led_index':117, 'state':0 }
stations[41220] = { 'name':' Fullerton (Red Brown & Purple lines)', 'eta':[], 'led_index':118, 'state':0 }
stations[40990] = { 'name':' 69th (Red Line)', 'eta':[], 'led_index':119, 'state':0 }
stations[41040] = { 'name':' Kedzie (Pink Line)', 'eta':[], 'led_index':120, 'state':0 }
stations[40800] = { 'name':' Sedgwick (Brown & Purple lines)', 'eta':[], 'led_index':121, 'state':0 }
stations[40060] = { 'name':' Belmont (Blue Line)', 'eta':[], 'led_index':122, 'state':0 }
stations[40840] = { 'name':' South Boulevard (Purple Line)', 'eta':[], 'led_index':123, 'state':0 }
stations[40170] = { 'name':' Ashland (Green & Pink lines)', 'eta':[], 'led_index':124, 'state':0 }
stations[40810] = { 'name':' Illinois Medical District (Blue Line)', 'eta':[], 'led_index':125, 'state':0 }
stations[41230] = { 'name':' 47th (Red Line)', 'eta':[], 'led_index':126, 'state':0 }
stations[41500] = { 'name':' Montrose (Brown Line)', 'eta':[], 'led_index':127, 'state':0 }
stations[40930] = { 'name':' Midway (Orange Line)', 'eta':[], 'led_index':128, 'state':0 }
stations[40550] = { 'name':' Irving Park (Blue Line)', 'eta':[], 'led_index':129, 'state':0 }
stations[40370] = { 'name':' Washington (Blue Line)', 'eta':[], 'led_index':130, 'state':0 }
stations[40890] = { 'name':' OHare (Blue Line)', 'eta':[], 'led_index':131, 'state':0 }
stations[40630] = { 'name':' Clark/Division (Red Line)', 'eta':[], 'led_index':132, 'state':0 }
stations[41420] = { 'name':' Addison (Red Line)', 'eta':[], 'led_index':133, 'state':0 }
stations[41290] = { 'name':' Kimball (Brown Line)', 'eta':[], 'led_index':134, 'state':0 }
stations[40300] = { 'name':' Indiana (Green Line)', 'eta':[], 'led_index':135, 'state':0 }
stations[40420] = { 'name':' Cicero (Pink Line)', 'eta':[], 'led_index':136, 'state':0 }
stations[40720] = { 'name':' Cottage Grove (Green Line)', 'eta':[], 'led_index':137, 'state':0 }
stations[41380] = { 'name':' Bryn Mawr (Red Line)', 'eta':[], 'led_index':138, 'state':0 }
stations[41450] = { 'name':' Chicago (Red Line)', 'eta':[], 'led_index':139, 'state':0 }
stations[40590] = { 'name':' Damen (Blue Line)', 'eta':[], 'led_index':140, 'state':0 }
stations[41180] = { 'name':' Kedzie (Brown Line)', 'eta':[], 'led_index':141, 'state':0 }
stations[40010] = { 'name':' Austin (Blue Line)', 'eta':[], 'led_index':142, 'state':0 }
stations[41310] = { 'name':' Paulina (Brown Line)', 'eta':[], 'led_index':143, 'state':0 }
stations[40970] = { 'name':' Cicero (Blue Line)', 'eta':[], 'led_index':144, 'state':0 }
# Check if already configured and load config for Wifi, API Key etc.
try:
with open("config.json", "r") as f:
config = json.load(f)
APIKEY = config.APIKEY
SSID = config.SSID
PASS = config.PASS
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
reconnect_wifi()
start_http_server() # Still launch server to allow changing SSID and config
demo = False
except:
# Failed to open file, launch AP mode
start_ap()
start_http_server()
# Halt waiting for config
while(True):
sleep(5)
# Function sets up all LEDs.
init_LEDs()
while(True):
# Synchronise NTP time if hasn't been done in 1 hour
if (time() - last_ntp_sync) > 3600:
reconnect_wifi()
try:
ntptime.settime()
print("Time synchronized successfully")
last_ntp_sync = time()
except Exception as e:
print("Error synchronizing time:", e)
# Have we queried API in the query_period seconds?
if (time() - last_api_query) > query_period:
# Query updated API
trains = (requests.get("https://lapi.transitchicago.com/api/1.0/ttpositions.aspx?key="+APIKEY+"&rt=Blue&outputType=JSON")).json()['ctatt']
# Timestamps are served in local Chicago time, convert to GMT for simplicity on this side.
print(f"Time of poll is: "+str(gmtime(time_from_iso(trains['tmst'])+(5*60*60))))
# Loop through all trains and update matching stations
for train in trains['route'][0]['train']:
nextStation = int(train['nextStaId'])
eta_time = time_from_iso(train['arrT'])+(5*60*60)
# Append the ETA, we rely on the station loop to remove old trains
stations[nextStation]['eta'].append(eta_time)
last_api_query = time() # uptime polling window
# Process the ETA for each station
for station, dict in stations.items(): # loop through all stations
if station == 40820: # Our test station for onboard LED
# Set LED state tracker to 0, if no trains, it'll stay 0.
stations[station]['state'] = 0
for index, eta in enumerate(dict['eta']): # multiple trains. Enumerate is used so we have the index to update the dict itself.
time_from_now = eta - time()
print('Train will arrive at'+dict['name']+' in '+ str(time_from_now) + ' seconds')
if time_from_now < distance_on:
stations[station]['state'] = 1
if time_from_now < -distance_on: # Remove old trains
stations[station]['eta'].pop(index)
# Set Station LED state
stations[station]['led_index'].value(stations[station]['state'])
sleep(1)