Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15cad37d5e | ||
|
|
658adc9baf | ||
|
|
ab110013c0 | ||
|
|
1b781e93bf | ||
|
|
6f2a926a27 |
No files matched your search
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
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'
|
||||||
Regular → Executable
+105
-154
@@ -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,149 +10,54 @@ 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():
|
|
||||||
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>
|
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):
|
||||||
except Exception as e:
|
return send_file('portal.html')
|
||||||
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)
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
|
async def CTA_Stations(config):
|
||||||
|
# Allocate first and second station
|
||||||
stations = {}
|
stations = {}
|
||||||
|
stations[0] = { 'id':config["LStation"], 'eta':[] }
|
||||||
|
stations[1] = { 'id':config["RStation"], 'eta':[] }
|
||||||
|
APIKEY = config["APIKEY"]
|
||||||
|
|
||||||
# Setup Neopixel
|
# Setup Neopixel
|
||||||
p = machine.Pin(20) # connected on pin 20
|
p = machine.Pin(20) # connected on pin 20
|
||||||
@@ -158,36 +66,16 @@ wled[0] = (0,0,0)
|
|||||||
wled[1] = (0,0,0)
|
wled[1] = (0,0,0)
|
||||||
wled.write()
|
wled.write()
|
||||||
|
|
||||||
# Check if already configured and load config for Wifi, API Key etc.
|
# Synchronize time of RTC with NTP
|
||||||
try:
|
last_ntp_sync = -3700 # Init as more than an hour ago
|
||||||
with open("config.json", "r") as f:
|
last_api_query = mktime([2000,1,1,0,0,0,0,0]) # Set last query default (will run on first run)
|
||||||
config = json.load(f)
|
query_period = 120 # Query the API at most every 2 minutes
|
||||||
APIKEY = config["APIKEY"]
|
distance_on = 10 # Time from station before turning on LED
|
||||||
SSID = config["SSID"]
|
distance_half = 30 # Time from station before going 'half' on
|
||||||
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....
|
while True:
|
||||||
#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)
|
|
||||||
|
|
||||||
while(True):
|
|
||||||
# Synchronise NTP time if hasn't been done in 1 hour
|
|
||||||
if (time() - last_ntp_sync) > 3600:
|
if (time() - last_ntp_sync) > 3600:
|
||||||
reconnect_wifi()
|
#reconnect_wifi()
|
||||||
try:
|
try:
|
||||||
ntptime.settime()
|
ntptime.settime()
|
||||||
print("Time synchronized successfully")
|
print("Time synchronized successfully")
|
||||||
@@ -197,15 +85,16 @@ while(True):
|
|||||||
|
|
||||||
# Have we queried API in the query_period seconds?
|
# Have we queried API in the query_period seconds?
|
||||||
if (time() - last_api_query) > query_period:
|
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
|
# Query all stations
|
||||||
for station,dict in stations.items():
|
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']
|
request = f"https://lapi.transitchicago.com/api/1.0/ttarrivals.aspx?key={APIKEY}&mapid={dict['id']}&outputType=JSON"
|
||||||
|
trains_s = (requests.get(request)).json()['ctatt']
|
||||||
|
|
||||||
# Timestamps are served in local Chicago time, convert to GMT for simplicity on this side.
|
# Zeroize all existing ETAs
|
||||||
#print(f"Time of poll is: "+str(gmtime(time_from_iso(trains_s['tmst'])+(5*60*60))))
|
stations[station]['eta'] = []
|
||||||
|
|
||||||
|
# Check if any trains, otherwise skip
|
||||||
|
if 'eta' in trains_s:
|
||||||
# Loop through all trains and update matching stations
|
# Loop through all trains and update matching stations
|
||||||
for train in trains_s['eta']:
|
for train in trains_s['eta']:
|
||||||
eta_time = time_from_iso(train['arrT'])+(5*60*60)
|
eta_time = time_from_iso(train['arrT'])+(5*60*60)
|
||||||
@@ -222,7 +111,11 @@ while(True):
|
|||||||
if (eta - time()) < -distance_on: # Remove old trains
|
if (eta - time()) < -distance_on: # Remove old trains
|
||||||
stations[station]['eta'].pop(index)
|
stations[station]['eta'].pop(index)
|
||||||
|
|
||||||
# Set LED state tracker to 0, if no trains, it'll stay 0.
|
# Set default
|
||||||
|
wled[station] = (0,0,0)
|
||||||
|
|
||||||
|
if len(value['eta']) > 0:
|
||||||
|
# Get closest train
|
||||||
eta = min(value['eta'])
|
eta = min(value['eta'])
|
||||||
time_from_now = eta - time()
|
time_from_now = eta - time()
|
||||||
print(f"Next train will arrive at {value['id']} in {str(time_from_now)} seconds")
|
print(f"Next train will arrive at {value['id']} in {str(time_from_now)} seconds")
|
||||||
@@ -230,10 +123,68 @@ while(True):
|
|||||||
wled[station] = (255,0,0)
|
wled[station] = (255,0,0)
|
||||||
elif time_from_now < distance_half:
|
elif time_from_now < distance_half:
|
||||||
wled[station] = (255,100,0)
|
wled[station] = (255,100,0)
|
||||||
else:
|
|
||||||
wled[station] = (0,0,0)
|
|
||||||
|
|
||||||
# Once all LEDs updated
|
# Once all LEDs updated
|
||||||
wled.write()
|
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 wlan.IF_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():
|
||||||
|
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)
|
sleep(1)
|
||||||
|
if wlan.isconnected():
|
||||||
|
print('Connected to Wi-Fi: ', SSID)
|
||||||
|
print('Device IP Address:', wlan.ifconfig()[0])
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
print('Connection error:', e)
|
||||||
|
print('Retrying connection...')
|
||||||
|
sleep(2)
|
||||||
|
except:
|
||||||
|
# Failed to open config file, launch AP mode and disable trains
|
||||||
|
demo = True
|
||||||
|
wlan = network.WLAN(network.AP_IF)
|
||||||
|
wlan.active(True)
|
||||||
|
|
||||||
|
wlan.config(
|
||||||
|
essid="CTA-Setup",
|
||||||
|
authmode=network.AUTH_OPEN
|
||||||
|
)
|
||||||
|
|
||||||
|
print("AP active for initial setup: ", wlan.active())
|
||||||
|
print("AP IP address:", wlan.ifconfig()[0])
|
||||||
|
|
||||||
|
|
||||||
|
asyncio.run(main())
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
File diff suppressed because it is too large.
Load diff
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
Regular → Executable
File mode changed.
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
Reference in new issue
Block a user