This commit is contained in:
Jimmy 2025-05-05 22:19:34 +12:00
parent 28fa71b8ad
commit 2fa02086c9
10 changed files with 786 additions and 0 deletions

19
src/boot.py Normal file
View File

@ -0,0 +1,19 @@
import wifi
import time
from settings import Settings
print(wifi.ap('qwerty'))
settings = Settings()
ssid = settings.get('wifi', {}).get('ssid', None)
password = settings.get('wifi', {}).get('password', None)
ip = settings.get('wifi', {}).get('ip', None)
gateway = settings.get('wifi', {}).get('gateway', None)
for i in range(10):
config = wifi.connect(ssid, password, ip, gateway)
if config:
print(config)
break
time.sleep(0.1)

55
src/main.py Normal file
View File

@ -0,0 +1,55 @@
import asyncio
from settings import Settings
from web import web
from patterns import Patterns
import gc
import utime
import machine
import ntptime
import time
import wifi
async def main():
settings = Settings()
patterns = Patterns(4, settings["num_leds"], selected=settings["selected_pattern"])
patterns.set_color1(tuple(int(settings["color1"][i:i+2], 16) for i in (1, 5, 3)))
patterns.set_color2(tuple(int(settings["color2"][i:i+2], 16) for i in (1, 5, 3)))
patterns.set_brightness(int(settings["brightness"]))
patterns.set_delay(int(settings["delay"]))
w = web(settings, patterns)
print(settings)
# start the server in a bacakground task
print("Starting")
server = asyncio.create_task(w.start_server(host="0.0.0.0", port=80))
wdt = machine.WDT(timeout=10000)
wdt.feed()
async def tick():
while True:
patterns.tick()
await asyncio.sleep_ms(1)
asyncio.create_task(tick())
first = True
while True:
#print(time.localtime())
# gc.collect()
for i in range(60):
wdt.feed()
await asyncio.sleep_ms(500)
# cleanup before ending the application
await server
asyncio.run(main())

53
src/settings.py Normal file
View File

@ -0,0 +1,53 @@
import json
class Settings(dict):
SETTINGS_FILE = "/settings.json"
def __init__(self):
super().__init__()
self.load() # Load settings from file during initialization
def set_defaults(self):
self["num_leds"] = 50
self["selected_pattern"] = "blink"
self["color1"] = "#000f00"
self["color2"] = "#0f0000"
self["delay"] = 100
self["brightness"] = 100
self["wifi"] = {"ssid": "", "password": ""}
def save(self):
try:
j = json.dumps(self)
with open(self.SETTINGS_FILE, 'w') as file:
file.write(j)
print("Settings saved successfully.")
except Exception as e:
print(f"Error saving settings: {e}")
def load(self):
try:
with open(self.SETTINGS_FILE, 'r') as file:
loaded_settings = json.load(file)
self.update(loaded_settings)
print("Settings loaded successfully.")
except Exception as e:
print(f"Error loading settings")
self.set_defaults()
# Example usage
def main():
settings = Settings()
print(f"Number of LEDs: {settings['num_leds']}")
settings['num_leds'] = 100
print(f"Updated number of LEDs: {settings['num_leds']}")
settings.save()
# Create a new Settings object to test loading
new_settings = Settings()
print(f"Loaded number of LEDs: {new_settings['num_leds']}")
print(settings)
# Run the example
if __name__ == "__main__":
main()

75
src/static/main.css Normal file
View File

@ -0,0 +1,75 @@
body {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 0 auto;
padding: 20px;
line-height: 1.6;
}
h1 {
text-align: center;
}
form {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
}
input[type="text"], input[type="submit"], input[type="range"], input[type="color"] {
width: 100%;
margin-bottom: 10px;
box-sizing: border-box;
}
input[type="range"] {
-webkit-appearance: none;
appearance: none;
height: 25px;
background: #d3d3d3;
outline: none;
opacity: 0.7;
transition: opacity .2s;
}
input[type="range"]:hover {
opacity: 1;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 25px;
height: 25px;
background: #4CAF50;
cursor: pointer;
border-radius: 50%;
}
input[type="range"]::-moz-range-thumb {
width: 25px;
height: 25px;
background: #4CAF50;
cursor: pointer;
border-radius: 50%;
}
#pattern_buttons {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 20px;
}
#pattern_buttons button {
flex: 1 0 calc(33.333% - 10px);
padding: 10px;
background-color: #4CAF50;
color: white;
border: none;
cursor: pointer;
transition: background-color 0.3s;
}
#pattern_buttons button:hover {
background-color: #45a049;
}
@media (max-width: 480px) {
#pattern_buttons button {
flex: 1 0 calc(50% - 10px);
}
}

156
src/static/main.js Normal file
View File

@ -0,0 +1,156 @@
let delayTimeout;
let brightnessTimeout;
let colorTimeout;
let color2Timeout;
let socket;
const host = window.location.host;
async function post(path, data) {
console.log(`POST to ${path}`, data);
try {
const response = await fetch(path, {
method: "POST",
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data) // Convert data to JSON string
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
} catch (error) {
console.error('Error during POST request:', error);
}
}
async function get(path) {
try {
const response = await fetch(path);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return await response.json(); // Assuming you are expecting JSON response
} catch (error) {
console.error('Error during GET request:', error);
}
}
async function updateColor(event) {
event.preventDefault();
clearTimeout(colorTimeout);
colorTimeout = setTimeout(async function() {
const color = document.getElementById('color').value;
await post("/color", { color }); // Send as JSON
}, 500);
}
async function updateColor2(event) {
event.preventDefault();
clearTimeout(color2Timeout);
color2Timeout = setTimeout(async function() {
const color = document.getElementById('color2').value;
await post("/color2", { color }); // Send as JSON
}, 500);
}
async function updatePattern(pattern) {
await post("/pattern", { pattern }); // Send as JSON
//socket.send(JSON.stringify({"selected_pattern":pattern}))
}
async function updateBrightness(event) {
event.preventDefault();
clearTimeout(brightnessTimeout);
brightnessTimeout = setTimeout(async function() {
const brightness = document.getElementById('brightness').value;
//await post('/brightness', { brightness }); // Send as JSON
}, 500);
}
async function updateDelay(event) {
event.preventDefault();
clearTimeout(delayTimeout);
delayTimeout = setTimeout(async function() {
const delay = document.getElementById('delay').value;
await post('/delay', { delay }); // Send as JSON
}, 500);
}
async function updateNumLeds(event) {
event.preventDefault();
const numLeds = document.getElementById('num_leds').value;
await post('/num_leds', { num_leds: numLeds }); // Send as JSON
}
async function updateWifi(event) {
event.preventDefault();
const ssid = document.getElementById('ssid').value;
const password = document.getElementById('password').value;
const ip = document.getElementById('ip').value;
const gateway = document.getElementById('gateway').value;
const wifiSettings = { ssid, password, ip, gateway }; // Create JSON object
console.log(wifiSettings);
const response = await post('/wifi_settings', wifiSettings); // Send as JSON
if (response === 500) {
alert("Failed to connect to Wi-Fi");
}
}
function createPatternButtons(patterns) {
const container = document.getElementById('pattern_buttons');
container.innerHTML = ''; // Clear previous buttons
patterns.forEach(pattern => {
const button = document.createElement('button');
button.type = 'button'; // Use 'button' instead of 'submit'
button.textContent = pattern;
button.value = pattern;
button.addEventListener('click', async function(event) {
event.preventDefault();
await updatePattern(pattern);
});
container.appendChild(button);
});
}
document.addEventListener('DOMContentLoaded', async function() {
document.getElementById('color').addEventListener('input', updateColor);
document.getElementById('color2').addEventListener('input', updateColor2);
document.getElementById('delay').addEventListener('input', updateDelay);
document.getElementById('brightness').addEventListener('input', updateBrightness);
document.getElementById('num_leds_form').addEventListener('submit', updateNumLeds);
document.getElementById('wifi_form').addEventListener('submit', updateWifi);
document.getElementById('delay').addEventListener('touchend', updateDelay);
document.getElementById('brightness').addEventListener('touchend', updateBrightness);
document.querySelectorAll(".pattern_button").forEach(button => {
console.log(button.value);
button.addEventListener('click', async event => {
event.preventDefault();
await updatePattern(button.value);
});
});
socket = new WebSocket(`ws://${host}/settings`)
});
// Function to toggle the display of the settings menu
function selectSettings() {
const settingsMenu = document.getElementById('settings_menu');
controls = document.getElementById('controls');
settingsMenu.style.display = 'block';
controls.style.display = 'none';
}
function selectControls() {
const settingsMenu = document.getElementById('settings_menu');
controls = document.getElementById('controls');
settingsMenu.style.display = 'none';
controls.style.display = 'block';
}

195
src/static/rgb-slider.js Normal file
View File

@ -0,0 +1,195 @@
// rgb-slider.js
export class RGBSlider extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: "open" });
shadow.innerHTML = `
<style>
.container {
display: flex;
flex-direction: column;
align-items: center;
padding: 1em;
border: 1px solid #ccc;
border-radius: 8px;
width: 50%;
font-family: sans-serif;
box-sizing: border-box;
}
.preview {
width: 50%;
height: 60px;
border-radius: 6px;
border: 1px solid #000;
background-color: rgb(0, 0, 0);
margin-bottom: 1em;
}
.sliders {
display: flex;
gap: 50px;
justify-content: center;
margin-bottom: 1em;
}
.slider-group {
display: flex;
flex-direction: column-reverse;
align-items: center;
}
.slider-group input[type="range"] {
writing-mode: vertical-lr;
direction: rtl;
width: 10px;
height: 120px;
}
.slider-group label {
margin-top: 8px;
font-size: 0.8em;
}
.rgb-inputs {
display: flex;
gap: 8px;
}
.rgb-inputs input {
width: 6ch;
padding: 2px;
font-family: monospace;
text-align: right;
}
.rgb-inputs label {
font-size: 0.8em;
text-align: center;
}
.rgb-input-group {
display: flex;
flex-direction: column;
align-items: center;
}
/* Mobile styles */
@media (max-width: 600px) {
.preview {
height: 80px;
}
.slider-group input[type="range"] {
height: 180px;
width: 14px;
}
.rgb-inputs input {
font-size: 1em;
padding: 4px;
width: 7ch;
}
.slider-group label,
.rgb-inputs label {
font-size: 1em;
}
.container {
padding: 1.5em;
}
}
</style>
<div class="container">
<div class="preview" id="preview"></div>
<div class="sliders">
<div class="slider-group">
<input type="range" min="0" max="255" value="0" id="r">
<label>R</label>
</div>
<div class="slider-group">
<input type="range" min="0" max="255" value="0" id="g">
<label>G</label>
</div>
<div class="slider-group">
<input type="range" min="0" max="255" value="0" id="b">
<label>B</label>
</div>
</div>
<div class="rgb-inputs">
<div class="rgb-input-group">
<label for="rInput">R</label>
<input type="number" min="0" max="255" id="rInput" value="0">
</div>
<div class="rgb-input-group">
<label for="gInput">G</label>
<input type="number" min="0" max="255" id="gInput" value="0">
</div>
<div class="rgb-input-group">
<label for="bInput">B</label>
<input type="number" min="0" max="255" id="bInput" value="0">
</div>
</div>
</div>
`;
const get = (id) => shadow.querySelector(id);
this.r = get("#r");
this.g = get("#g");
this.b = get("#b");
this.rInput = get("#rInput");
this.gInput = get("#gInput");
this.bInput = get("#bInput");
this.preview = get("#preview");
const updateColor = (r, g, b) => {
this.preview.style.backgroundColor = `rgb(${r}, ${g}, ${b})`;
this.rInput.value = r;
this.gInput.value = g;
this.bInput.value = b;
this.dispatchEvent(
new CustomEvent("color-change", {
detail: { r, g, b },
bubbles: true,
composed: true,
}),
);
};
const syncFromSliders = () => {
const r = +this.r.value;
const g = +this.g.value;
const b = +this.b.value;
updateColor(r, g, b);
};
const syncFromInputs = () => {
const r = Math.min(255, Math.max(0, +this.rInput.value));
const g = Math.min(255, Math.max(0, +this.gInput.value));
const b = Math.min(255, Math.max(0, +this.bInput.value));
this.r.value = r;
this.g.value = g;
this.b.value = b;
updateColor(r, g, b);
};
this.r.addEventListener("input", syncFromSliders);
this.g.addEventListener("input", syncFromSliders);
this.b.addEventListener("input", syncFromSliders);
this.rInput.addEventListener("change", syncFromInputs);
this.gInput.addEventListener("change", syncFromInputs);
this.bInput.addEventListener("change", syncFromInputs);
}
}
customElements.define("rgb-slider", RGBSlider);

71
src/templates/index.html Normal file
View File

@ -0,0 +1,71 @@
{% args settings, patterns %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LED Control</title>
<script src="static/main.js"></script>
<link rel="stylesheet" href="static/main.css">
</head>
<body>
<h1>Control LEDs</h1>
<button onclick="selectControls()">Controls</button>
<button onclick="selectSettings()">Settings</button>
<!-- Main LED Controls -->
<div id="controls">
<div id="pattern_buttons">
{% for p in patterns %}
<button class="pattern_button" value="{{p}}">{{p}}</button>
{% endfor %}
<!-- Pattern buttons will be inserted here -->
</div>
<form id="delay_form" method="post" action="/delay">
<label for="delay">Delay:</label>
<input type="range" id="delay" name="delay" min="1" max="1000" value="{{settings['delay']}}" step="10">
</form>
<form id="brightness_form" method="post" action="/brightness">
<label for="brightness">Brightness:</label>
<input type="range" id="brightness" name="brightness" min="0" max="100" value="{{settings['brightness']}}" step="1">
</form>
<form id="color_form" method="post" action="/color">
<input type="color" id="color" name="color" value="{{settings['color1']}}">
</form>
<form id="color2_form" method="post" action="/color2">
<input type="color" id="color2" name="color2" value="{{settings['color2']}}">
</form>
</div>
<!-- Settings Menu for num_leds, Wi-Fi SSID, and Password -->
<div id="settings_menu" style="display: none;">
<h2>Settings</h2>
<!-- Separate form for submitting num_leds -->
<form id="num_leds_form" method="post" action="/num_leds">
<label for="num_leds">Number of LEDs:</label>
<input type="text" id="num_leds" name="num_leds" value="{{settings['num_leds']}}">
<input type="submit" value="Update Number of LEDs">
</form>
<!-- Form for Wi-Fi SSID and password -->
<form id="wifi_form" method="post" action="/wifi_settings">
<label for="ssid">Wi-Fi SSID:</label>
<input type="text" id="ssid" name="ssid" value="{{settings['wifi']['ssid']}}">
<br>
<label for="password">Wi-Fi Password:</label>
<input type="password" id="password" name="password">
<br>
<label for="ip">Wi-Fi IP:</label>
<input type="ip" id="ip" name="ip" value="{{settings.get('wifi', {}).get('ip', '')}}">
<br>
<label for="gateway">Wi-Fi Gateway:</label>
<input type="gateway" id="gateway" name="gateway" value="{{settings.get('wifi', {}).get('gateway', '')}}">
<br>
<input type="submit" value="Save Wi-Fi Settings">
</form>
</div>
</body>
</html>

View File

@ -0,0 +1,94 @@
# Autogenerated file
def render(settings, patterns):
yield """<!DOCTYPE html>
<html lang=\"en\">
<head>
<meta charset=\"UTF-8\">
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">
<title>LED Control</title>
<script src=\"static/main.js\"></script>
<link rel=\"stylesheet\" href=\"static/main.css\">
</head>
<body>
<h1>Control LEDs</h1>
<button onclick=\"selectControls()\">Controls</button>
<button onclick=\"selectSettings()\">Settings</button>
<!-- Main LED Controls -->
<div id=\"controls\">
<div id=\"pattern_buttons\">
"""
for p in patterns:
yield """ <button class=\"pattern_button\" value=\""""
yield str(p)
yield """\">"""
yield str(p)
yield """</button>
"""
yield """
<!-- Pattern buttons will be inserted here -->
</div>
<form id=\"delay_form\" method=\"post\" action=\"/delay\">
<label for=\"delay\">Delay:</label>
<input type=\"range\" id=\"delay\" name=\"delay\" min=\"1\" max=\"1000\" value=\""""
yield str(settings['delay'])
yield """\" step=\"10\">
</form>
<form id=\"brightness_form\" method=\"post\" action=\"/brightness\">
<label for=\"brightness\">Brightness:</label>
<input type=\"range\" id=\"brightness\" name=\"brightness\" min=\"0\" max=\"100\" value=\""""
yield str(settings['brightness'])
yield """\" step=\"1\">
</form>
<form id=\"color_form\" method=\"post\" action=\"/color\">
<input type=\"color\" id=\"color\" name=\"color\" value=\""""
yield str(settings['color1'])
yield """\">
</form>
<form id=\"color2_form\" method=\"post\" action=\"/color2\">
<input type=\"color\" id=\"color2\" name=\"color2\" value=\""""
yield str(settings['color2'])
yield """\">
</form>
</div>
<!-- Settings Menu for num_leds, Wi-Fi SSID, and Password -->
<div id=\"settings_menu\" style=\"display: none;\">
<h2>Settings</h2>
<!-- Separate form for submitting num_leds -->
<form id=\"num_leds_form\" method=\"post\" action=\"/num_leds\">
<label for=\"num_leds\">Number of LEDs:</label>
<input type=\"text\" id=\"num_leds\" name=\"num_leds\" value=\""""
yield str(settings['num_leds'])
yield """\">
<input type=\"submit\" value=\"Update Number of LEDs\">
</form>
<!-- Form for Wi-Fi SSID and password -->
<form id=\"wifi_form\" method=\"post\" action=\"/wifi_settings\">
<label for=\"ssid\">Wi-Fi SSID:</label>
<input type=\"text\" id=\"ssid\" name=\"ssid\" value=\""""
yield str(settings['wifi']['ssid'])
yield """\">
<br>
<label for=\"password\">Wi-Fi Password:</label>
<input type=\"password\" id=\"password\" name=\"password\">
<br>
<label for=\"ip\">Wi-Fi IP:</label>
<input type=\"ip\" id=\"ip\" name=\"ip\" value=\""""
yield str(settings.get('wifi', {}).get('ip', ''))
yield """\">
<br>
<label for=\"gateway\">Wi-Fi Gateway:</label>
<input type=\"gateway\" id=\"gateway\" name=\"gateway\" value=\""""
yield str(settings.get('wifi', {}).get('gateway', ''))
yield """\">
<br>
<input type=\"submit\" value=\"Save Wi-Fi Settings\">
</form>
</div>
</body>
</html>
"""

22
src/web.py Normal file
View File

@ -0,0 +1,22 @@
from microdot import Microdot, send_file, Response
from microdot.utemplate import Template
from microdot.websocket import with_websocket
import json
import wifi
def web(settings):
app = Microdot()
Response.default_content_type = 'text/html'
@app.route('/')
async def index(request):
return Template('/index.html').render(settings=settings, patterns=patterns.patterns.keys())
@app.route("/static/<path:path>")
def static(request, path):
if '..' in path:
# Directory traversal is not allowed
return 'Not found', 404
return send_file('static/' + path)
return app

46
src/wifi.py Normal file
View File

@ -0,0 +1,46 @@
import network
from machine import Pin
from time import sleep
import ubinascii
from settings import Settings
def connect(ssid, password, ip, gateway):
if ssid is None or password is None:
print("Missing ssid or password")
return None
try:
sta_if = network.WLAN(network.STA_IF)
if ip is not None and gateway is not None:
sta_if.ifconfig((ip, '255.255.255.0', gateway, '1.1.1.1'))
if not sta_if.isconnected():
print('connecting to network...')
sta_if.active(True)
sta_if.connect(ssid, password)
sleep(0.1)
if sta_if.isconnected():
return sta_if.ifconfig()
return None
return sta_if.ifconfig()
except Exception as e:
print(f"Failed to connect to wifi {e}")
return None
def ap(password):
ap_if = network.WLAN(network.AP_IF)
ap_mac = ap_if.config('mac')
ssid = f"led-{ubinascii.hexlify(ap_mac).decode()}"
print(ssid)
ap_if.active(True)
ap_if.config(essid=ssid, password="qwerty1234")
ap_if.active(False)
ap_if.active(True)
print(ap_if.ifconfig())