Update main application and dependencies
- Update main.py and run_web.py for local development - Update microdot session handling - Update wifi utility
This commit is contained in:
@@ -1,4 +1,25 @@
|
|||||||
|
try:
|
||||||
import jwt
|
import jwt
|
||||||
|
HAS_JWT = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_JWT = False
|
||||||
|
try:
|
||||||
|
import ubinascii
|
||||||
|
except ImportError:
|
||||||
|
import binascii as ubinascii
|
||||||
|
try:
|
||||||
|
import uhashlib as hashlib
|
||||||
|
except ImportError:
|
||||||
|
import hashlib
|
||||||
|
try:
|
||||||
|
import uhmac as hmac
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
import hmac
|
||||||
|
except ImportError:
|
||||||
|
hmac = None
|
||||||
|
import json
|
||||||
|
|
||||||
from microdot.microdot import invoke_handler
|
from microdot.microdot import invoke_handler
|
||||||
from microdot.helpers import wraps
|
from microdot.helpers import wraps
|
||||||
|
|
||||||
@@ -125,16 +146,61 @@ class Session:
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
def encode(self, payload, secret_key=None):
|
def encode(self, payload, secret_key=None):
|
||||||
|
"""Encode session data using JWT if available, otherwise use simple HMAC."""
|
||||||
|
if HAS_JWT:
|
||||||
return jwt.encode(payload, secret_key or self.secret_key,
|
return jwt.encode(payload, secret_key or self.secret_key,
|
||||||
algorithm='HS256')
|
algorithm='HS256')
|
||||||
|
else:
|
||||||
|
# Simple encoding for MicroPython: base64(json) + HMAC signature
|
||||||
|
key = (secret_key or self.secret_key).encode() if isinstance(secret_key or self.secret_key, str) else (secret_key or self.secret_key)
|
||||||
|
payload_json = json.dumps(payload)
|
||||||
|
payload_b64 = ubinascii.b2a_base64(payload_json.encode()).decode().strip()
|
||||||
|
|
||||||
|
# Create HMAC signature
|
||||||
|
if hmac:
|
||||||
|
# Use hmac module if available
|
||||||
|
h = hmac.new(key, payload_json.encode(), hashlib.sha256)
|
||||||
|
else:
|
||||||
|
# Fallback: simple SHA256(key + message)
|
||||||
|
h = hashlib.sha256(key + payload_json.encode())
|
||||||
|
signature = ubinascii.b2a_base64(h.digest()).decode().strip()
|
||||||
|
|
||||||
|
return f"{payload_b64}.{signature}"
|
||||||
|
|
||||||
def decode(self, session, secret_key=None):
|
def decode(self, session, secret_key=None):
|
||||||
|
"""Decode session data using JWT if available, otherwise use simple HMAC."""
|
||||||
|
if HAS_JWT:
|
||||||
try:
|
try:
|
||||||
payload = jwt.decode(session, secret_key or self.secret_key,
|
payload = jwt.decode(session, secret_key or self.secret_key,
|
||||||
algorithms=['HS256'])
|
algorithms=['HS256'])
|
||||||
except jwt.exceptions.PyJWTError: # pragma: no cover
|
except jwt.exceptions.PyJWTError: # pragma: no cover
|
||||||
return {}
|
return {}
|
||||||
return payload
|
return payload
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
# Simple decoding for MicroPython
|
||||||
|
if '.' not in session:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
payload_b64, signature = session.rsplit('.', 1)
|
||||||
|
payload_json = ubinascii.a2b_base64(payload_b64).decode()
|
||||||
|
|
||||||
|
# Verify HMAC signature
|
||||||
|
key = (secret_key or self.secret_key).encode() if isinstance(secret_key or self.secret_key, str) else (secret_key or self.secret_key)
|
||||||
|
if hmac:
|
||||||
|
# Use hmac module if available
|
||||||
|
h = hmac.new(key, payload_json.encode(), hashlib.sha256)
|
||||||
|
else:
|
||||||
|
# Fallback: simple SHA256(key + message)
|
||||||
|
h = hashlib.sha256(key + payload_json.encode())
|
||||||
|
expected_signature = ubinascii.b2a_base64(h.digest()).decode().strip()
|
||||||
|
|
||||||
|
if signature != expected_signature:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
return json.loads(payload_json)
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def with_session(f):
|
def with_session(f):
|
||||||
|
|||||||
16
run_web.py
16
run_web.py
@@ -87,6 +87,8 @@ async def run_local():
|
|||||||
from microdot import Microdot, send_file
|
from microdot import Microdot, send_file
|
||||||
from microdot.websocket import with_websocket
|
from microdot.websocket import with_websocket
|
||||||
|
|
||||||
|
from microdot.session import Session
|
||||||
|
|
||||||
import controllers.preset as preset
|
import controllers.preset as preset
|
||||||
import controllers.profile as profile
|
import controllers.profile as profile
|
||||||
import controllers.group as group
|
import controllers.group as group
|
||||||
@@ -94,9 +96,15 @@ async def run_local():
|
|||||||
import controllers.tab as tab
|
import controllers.tab as tab
|
||||||
import controllers.palette as palette
|
import controllers.palette as palette
|
||||||
import controllers.scene as scene
|
import controllers.scene as scene
|
||||||
|
import controllers.pattern as pattern
|
||||||
|
import controllers.settings as settings_controller
|
||||||
|
|
||||||
app = Microdot()
|
app = Microdot()
|
||||||
|
|
||||||
|
# Initialize sessions with a secret key from settings
|
||||||
|
secret_key = settings.get('session_secret_key', 'led-controller-secret-key-change-in-production')
|
||||||
|
Session(app, secret_key=secret_key)
|
||||||
|
|
||||||
# Mount model controllers as subroutes
|
# Mount model controllers as subroutes
|
||||||
app.mount(preset.controller, '/presets')
|
app.mount(preset.controller, '/presets')
|
||||||
app.mount(profile.controller, '/profiles')
|
app.mount(profile.controller, '/profiles')
|
||||||
@@ -105,6 +113,8 @@ async def run_local():
|
|||||||
app.mount(tab.controller, '/tabs')
|
app.mount(tab.controller, '/tabs')
|
||||||
app.mount(palette.controller, '/palettes')
|
app.mount(palette.controller, '/palettes')
|
||||||
app.mount(scene.controller, '/scenes')
|
app.mount(scene.controller, '/scenes')
|
||||||
|
app.mount(pattern.controller, '/patterns')
|
||||||
|
app.mount(settings_controller.controller, '/settings')
|
||||||
|
|
||||||
# Serve index.html at root
|
# Serve index.html at root
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
@@ -112,6 +122,12 @@ async def run_local():
|
|||||||
"""Serve the main web UI."""
|
"""Serve the main web UI."""
|
||||||
return send_file('src/templates/index.html')
|
return send_file('src/templates/index.html')
|
||||||
|
|
||||||
|
# Serve settings page
|
||||||
|
@app.route('/settings')
|
||||||
|
def settings_page(request):
|
||||||
|
"""Serve the settings page."""
|
||||||
|
return send_file('src/templates/settings.html')
|
||||||
|
|
||||||
# Static file route
|
# Static file route
|
||||||
@app.route("/static/<path:path>")
|
@app.route("/static/<path:path>")
|
||||||
def static_handler(request, path):
|
def static_handler(request, path):
|
||||||
|
|||||||
14
src/main.py
14
src/main.py
@@ -16,6 +16,7 @@ import controllers.tab as tab
|
|||||||
import controllers.palette as palette
|
import controllers.palette as palette
|
||||||
import controllers.scene as scene
|
import controllers.scene as scene
|
||||||
import controllers.pattern as pattern
|
import controllers.pattern as pattern
|
||||||
|
import controllers.settings as settings_controller
|
||||||
|
|
||||||
|
|
||||||
async def main(port=80):
|
async def main(port=80):
|
||||||
@@ -56,6 +57,7 @@ async def main(port=80):
|
|||||||
app.mount(palette.controller, '/palettes')
|
app.mount(palette.controller, '/palettes')
|
||||||
app.mount(scene.controller, '/scenes')
|
app.mount(scene.controller, '/scenes')
|
||||||
app.mount(pattern.controller, '/patterns')
|
app.mount(pattern.controller, '/patterns')
|
||||||
|
app.mount(settings_controller.controller, '/settings')
|
||||||
|
|
||||||
# Serve index.html at root
|
# Serve index.html at root
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
@@ -63,6 +65,12 @@ async def main(port=80):
|
|||||||
"""Serve the main web UI."""
|
"""Serve the main web UI."""
|
||||||
return send_file('templates/index.html')
|
return send_file('templates/index.html')
|
||||||
|
|
||||||
|
# Serve settings page
|
||||||
|
@app.route('/settings')
|
||||||
|
def settings_page(request):
|
||||||
|
"""Serve the settings page."""
|
||||||
|
return send_file('templates/settings.html')
|
||||||
|
|
||||||
# Static file route
|
# Static file route
|
||||||
@app.route("/static/<path:path>")
|
@app.route("/static/<path:path>")
|
||||||
def static_handler(request, path):
|
def static_handler(request, path):
|
||||||
@@ -87,13 +95,13 @@ async def main(port=80):
|
|||||||
|
|
||||||
server = asyncio.create_task(app.start_server(host="0.0.0.0", port=port))
|
server = asyncio.create_task(app.start_server(host="0.0.0.0", port=port))
|
||||||
|
|
||||||
wdt = machine.WDT(timeout=10000)
|
#wdt = machine.WDT(timeout=10000)
|
||||||
wdt.feed()
|
#wdt.feed()
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
gc.collect()
|
gc.collect()
|
||||||
for i in range(60):
|
for i in range(60):
|
||||||
wdt.feed()
|
#wdt.feed()
|
||||||
await asyncio.sleep_ms(500)
|
await asyncio.sleep_ms(500)
|
||||||
# cleanup before ending the application
|
# cleanup before ending the application
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ import network
|
|||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
def connect(ssid, password, ip, gateway):
|
def connect(ssid, password, ip, gateway):
|
||||||
if ssid is None or password is None:
|
if ssid is None:
|
||||||
print("Missing ssid or password")
|
print("Missing ssid")
|
||||||
return None
|
return None
|
||||||
|
if password is None:
|
||||||
|
password = ''
|
||||||
try:
|
try:
|
||||||
sta_if = network.WLAN(network.STA_IF)
|
sta_if = network.WLAN(network.STA_IF)
|
||||||
if ip is not None and gateway is not None:
|
if ip is not None and gateway is not None:
|
||||||
@@ -23,11 +25,14 @@ def connect(ssid, password, ip, gateway):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def ap(ssid, password):
|
def ap(ssid, password, channel=None):
|
||||||
ap_if = network.WLAN(network.AP_IF)
|
ap_if = network.WLAN(network.AP_IF)
|
||||||
ap_mac = ap_if.config('mac')
|
ap_mac = ap_if.config('mac')
|
||||||
print(ssid)
|
print(ssid)
|
||||||
ap_if.active(True)
|
ap_if.active(True)
|
||||||
|
if channel is not None:
|
||||||
|
ap_if.config(essid=ssid, password=password, channel=channel)
|
||||||
|
else:
|
||||||
ap_if.config(essid=ssid, password=password)
|
ap_if.config(essid=ssid, password=password)
|
||||||
ap_if.active(False)
|
ap_if.active(False)
|
||||||
ap_if.active(True)
|
ap_if.active(True)
|
||||||
@@ -36,3 +41,60 @@ def ap(ssid, password):
|
|||||||
def get_mac():
|
def get_mac():
|
||||||
ap_if = network.WLAN(network.AP_IF)
|
ap_if = network.WLAN(network.AP_IF)
|
||||||
return ap_if.config('mac')
|
return ap_if.config('mac')
|
||||||
|
|
||||||
|
def get_ap_config():
|
||||||
|
"""Get current AP configuration."""
|
||||||
|
try:
|
||||||
|
ap_if = network.WLAN(network.AP_IF)
|
||||||
|
if ap_if.active():
|
||||||
|
config = ap_if.ifconfig()
|
||||||
|
return {
|
||||||
|
'ssid': ap_if.config('essid'),
|
||||||
|
'channel': ap_if.config('channel'),
|
||||||
|
'ip': config[0] if config else None,
|
||||||
|
'active': True
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
'ssid': None,
|
||||||
|
'channel': None,
|
||||||
|
'ip': None,
|
||||||
|
'active': False
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error getting AP config: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_sta_status():
|
||||||
|
"""Get current station connection status."""
|
||||||
|
try:
|
||||||
|
sta_if = network.WLAN(network.STA_IF)
|
||||||
|
if sta_if.active():
|
||||||
|
if sta_if.isconnected():
|
||||||
|
config = sta_if.ifconfig()
|
||||||
|
return {
|
||||||
|
'connected': True,
|
||||||
|
'ssid': sta_if.config('essid'),
|
||||||
|
'ip': config[0] if config else None,
|
||||||
|
'gateway': config[2] if len(config) > 2 else None,
|
||||||
|
'netmask': config[1] if len(config) > 1 else None,
|
||||||
|
'dns': config[3] if len(config) > 3 else None
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
'connected': False,
|
||||||
|
'ssid': None,
|
||||||
|
'ip': None,
|
||||||
|
'gateway': None,
|
||||||
|
'netmask': None,
|
||||||
|
'dns': None
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
'connected': False,
|
||||||
|
'ssid': None,
|
||||||
|
'ip': None,
|
||||||
|
'gateway': None,
|
||||||
|
'netmask': None,
|
||||||
|
'dns': None
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error getting STA status: {e}")
|
||||||
|
return None
|
||||||
|
|||||||
Reference in New Issue
Block a user