Compare commits

..
Author SHA1 Message Date
= 9d468f0414 Updates for fixed AP detect 2026-10-11 18:48:44 +10:00
= efc1114c86 Updates for async 2026-10-11 18:35:28 +10:00
iosullivan 15cad37d5e Changes to be committed:
modified:   examples/src/micropython-webserver/main.py
2026-10-11 18:33:01 +10:00
iosullivan 658adc9baf Updates for asyncio logic example 2026-10-11 18:30:26 +10:00
iosullivan ab110013c0 Updates further 2026-10-10 19:55:04 +10:00
iosullivan 1b781e93bf Updates for async 2026-10-10 19:28:04 +10:00
iosullivan 6f2a926a27 Updates for Async 2026-10-10 18:58:02 +10:00
14 changed files with 1736 additions and 190 deletions

No files matched your search

Regular → Executable
View File
File mode changed.
Regular → Executable
View File
File mode changed.
View File
File mode changed.
View File
File mode changed.
View File
File mode changed.
@@ -0,0 +1,4 @@
from microdot.microdot import Microdot, Request, Response, abort, redirect, \
send_file, URLPattern, AsyncBytesIO, iscoroutine # noqa: F401
__version__ = '2.7.0'
+136 -190
View File
@@ -1,5 +1,8 @@
from time import time, gmtime, mktime,sleep from time import time, gmtime, mktime,sleep
import network, machine, ntptime, socket, json, requests,neopixel import network, machine, ntptime, socket, json, requests, neopixel, asyncio
from microdot import Microdot, send_file
app = Microdot()
def time_from_iso(datetime_str): def time_from_iso(datetime_str):
[yyyy, mm, dd] = [int(i) for i in datetime_str.split('T')[0].split('-')] [yyyy, mm, dd] = [int(i) for i in datetime_str.split('T')[0].split('-')]
@@ -7,122 +10,144 @@ def time_from_iso(datetime_str):
time_u = mktime((yyyy, mm, dd, h, m, s, 0, 0)) time_u = mktime((yyyy, mm, dd, h, m, s, 0, 0))
return time_u return time_u
def start_ap(): @app.post('/save')
# Sets up initial captive portal for configuring wifi and basic options async def save(request):
ap = network.WLAN(network.AP_IF) # Handle form submission
ap.active(True) response = request.form
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""" """Save Wi-Fi credentials to config.json"""
if not data.get("SSID"): if not response["SSID"]:
raise ValueError("SSID is required") raise ValueError("SSID is required")
config = { config = {
"SSID": data.get("SSID"), "SSID": response["SSID"],
"PASS": data.get("PASS", ""), "PASS": response["PASS"],
"APIKEY": data.get("APIKEY", ""), "APIKEY": response["APIKEY"],
"LStation": data.get("LStation", ""), "LStation": response["LStation"],
"RStation": data.get("RStation", "") "RStation": response["RStation"]
} }
with open("config.json", "w") as f: with open("config.json", "w") as f:
json.dump(config, f) json.dump(config, f)
print("Configuration saved:", config)
def start_http_server(): response_body = """<!DOCTYPE html>
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> <html>
<head> <head>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Setup Complete</title> <title>Initial Setup Complete</title>
<style> <style>
body { font-family: sans-serif; padding: 16px; text-align: center; } body { font-family: sans-serif; padding: 16px; text-align: center; }
h2 { color: #28a745; } h2 { color: #28a745; }
</style> </style>
</head> </head>
<body> <body>
<h2>Configuration saved</h2> <h2>Configuration Saved</h2>
<p>Device will reboot and connect to Wi-Fi.</p> <p>Reboot device to attempt Wi-Fi connection.</p>
<p><small>You can close this window.</small></p>
</body> </body>
</html>""" </html>"""
else:
# Serve the setup page for any other request
try:
with open("portal.html") as f:
response_body = f.read()
except:
response_body = "<h1>Setup Page Not Found</h1><p>Please upload portal.html</p>"
response = ( return response_body, {'Content-Type': 'text/html'}
"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()) @app.get('/')
async def index(request):
return send_file('portal.html')
except Exception as e: async def CTA_Stations(config):
print("HTTP error:", e) # Allocate first and second station
stations = {}
stations[0] = { 'id':config["LStation"], 'eta':[] }
stations[1] = { 'id':config["RStation"], 'eta':[] }
APIKEY = config["APIKEY"]
# Setup Neopixel
p = machine.Pin(20) # connected on pin 20
wled = neopixel.NeoPixel(p,2)
wled[0] = (0,0,0)
wled[1] = (0,0,0)
wled.write()
# 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
distance_half = 30 # Time from station before going 'half' on
while True:
if (time() - last_ntp_sync) > 3600:
#reconnect_wifi()
try: try:
error = f"<h2>Error</h2><p>{e}</p>" ntptime.settime()
conn.send( print("Time synchronized successfully")
"HTTP/1.1 500 Internal Server Error\r\n" last_ntp_sync = time()
"Content-Type: text/html\r\n" except Exception as e:
"Connection: close\r\n\r\n" print("Error synchronizing time:", e)
.encode() + error.encode()
) # Have we queried API in the query_period seconds?
except: if (time() - last_api_query) > query_period:
pass # Query all stations
finally: for station,dict in stations.items():
try: request = f"https://lapi.transitchicago.com/api/1.0/ttarrivals.aspx?key={APIKEY}&mapid={dict['id']}&outputType=JSON"
conn.close() trains_s = (requests.get(request)).json()['ctatt']
except:
pass
def reconnect_wifi(): # Zeroize all existing ETAs
stations[station]['eta'] = []
# Check if any trains, otherwise skip
if 'eta' in trains_s:
# Loop through all trains and update matching stations
for train in trains_s['eta']:
eta_time = time_from_iso(train['arrT'])+(5*60*60)
# Append the ETA, we rely on the station loop to remove old trains
stations[station]['eta'].append(eta_time)
last_api_query = time() # uptime polling window
# Process the ETA for each station
for station, value in stations.items(): # loop through all stations
for index, eta in enumerate(value['eta']): # multiple trains. Find old trains
if (eta - time()) < -distance_on: # Remove old trains
stations[station]['eta'].pop(index)
# Set default
wled[station] = (0,0,0)
if len(value['eta']) > 0:
# Get closest train
eta = min(value['eta'])
time_from_now = eta - time()
print(f"Next train will arrive at {value['id']} in {str(time_from_now)} seconds")
if time_from_now < distance_on:
wled[station] = (255,0,0)
elif time_from_now < distance_half:
wled[station] = (255,100,0)
# Once all LEDs updated
wled.write()
await asyncio.sleep(1)
async def main():
server = asyncio.create_task(app.start_server(port=80))
# Only run if we have internet
if mode is "STA":
asyncio.create_task(CTA_Stations(config))
await server
# Check if already configured and load config for Wifi, API Key etc.
try:
with open("config.json", "r") as f:
config = json.load(f)
SSID = config["SSID"]
PASS = config["PASS"]
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
while not wlan.isconnected(): while not wlan.isconnected():
try: try:
wlan.connect(SSID, PASS) wlan.connect(SSID, PASS)
@@ -134,106 +159,27 @@ def reconnect_wifi():
print('Waiting for connection...') print('Waiting for connection...')
sleep(1) sleep(1)
if wlan.isconnected(): if wlan.isconnected():
print('Connected to Wi-Fi') print('Connected to Wi-Fi: ', SSID)
print('Network config:', wlan.ifconfig()) print('Device IP Address:', wlan.ifconfig()[0])
break break
except Exception as e: except Exception as e:
print('Connection error:', e) print('Connection error:', e)
print('Retrying connection...') print('Retrying connection...')
sleep(2) sleep(2)
mode = "STA"
# 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
distance_half = 30 # Time from station before going 'half' on
stations = {}
# Setup Neopixel
p = machine.Pin(20) # connected on pin 20
wled = neopixel.NeoPixel(p,2)
wled[0] = (0,0,0)
wled[1] = (0,0,0)
wled.write()
# 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"]
# Allocate first and second station in demo only
stations[0] = { 'id':config["LStation"], 'eta':[] }
stations[1] = { 'id':config["RStation"], 'eta':[] }
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
reconnect_wifi()
# need to make below multithreaded....
#start_http_server() # Still launch server to allow changing SSID and config
demo = False
except: except:
# Failed to open file, launch AP mode # Failed to open config file, launch AP mode and disable trains
start_ap() demo = True
start_http_server() wlan = network.WLAN(network.AP_IF)
wlan.active(True)
# Halt waiting for config wlan.config(
while(True): essid="CTA-Setup",
sleep(5) authmode=network.AUTH_OPEN
)
while(True): print("AP active for initial setup: ", wlan.active())
# Synchronise NTP time if hasn't been done in 1 hour print("AP IP address:", wlan.ifconfig()[0])
if (time() - last_ntp_sync) > 3600: mode = "AP"
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']
# Query all stations
for station,dict in stations.items():
trains_s = (requests.get(f"https://lapi.transitchicago.com/api/1.0/ttarrivals.aspx?key={APIKEY}&mapid={dict['id']}&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_s['tmst'])+(5*60*60))))
# Loop through all trains and update matching stations
for train in trains_s['eta']:
eta_time = time_from_iso(train['arrT'])+(5*60*60)
# Append the ETA, we rely on the station loop to remove old trains
stations[station]['eta'].append(eta_time)
last_api_query = time() # uptime polling window
# Process the ETA for each station
for station, value in stations.items(): # loop through all stations
for index, eta in enumerate(value['eta']): # multiple trains. Find old trains
if (eta - time()) < -distance_on: # Remove old trains
stations[station]['eta'].pop(index)
# Set LED state tracker to 0, if no trains, it'll stay 0.
eta = min(value['eta'])
time_from_now = eta - time()
print(f"Next train will arrive at {value['id']} in {str(time_from_now)} seconds")
if time_from_now < distance_on:
wled[station] = (255,0,0)
elif time_from_now < distance_half:
wled[station] = (255,100,0)
else:
wled[station] = (0,0,0)
# Once all LEDs updated asyncio.run(main())
wled.write()
sleep(1)
File diff suppressed because it is too large. Load diff
View File
File mode changed.
View File
File mode changed.
View File
File mode changed.
Regular → Executable
View File
File mode changed.
Regular → Executable
View File
File mode changed.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Regular → Executable
View File
File mode changed.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB