diff --git a/src/boot.py b/src/boot.py new file mode 100644 index 0000000..bd892c1 --- /dev/null +++ b/src/boot.py @@ -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) \ No newline at end of file diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..4e8b7ff --- /dev/null +++ b/src/main.py @@ -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()) + + diff --git a/src/settings.py b/src/settings.py new file mode 100644 index 0000000..7ad5c81 --- /dev/null +++ b/src/settings.py @@ -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() \ No newline at end of file diff --git a/src/static/main.css b/src/static/main.css new file mode 100644 index 0000000..0c56bb7 --- /dev/null +++ b/src/static/main.css @@ -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); + } + } \ No newline at end of file diff --git a/src/static/main.js b/src/static/main.js new file mode 100644 index 0000000..73d7374 --- /dev/null +++ b/src/static/main.js @@ -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'; +} diff --git a/src/static/rgb-slider.js b/src/static/rgb-slider.js new file mode 100644 index 0000000..0c986df --- /dev/null +++ b/src/static/rgb-slider.js @@ -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); diff --git a/src/templates/index.html b/src/templates/index.html new file mode 100644 index 0000000..57b9eac --- /dev/null +++ b/src/templates/index.html @@ -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> diff --git a/src/templates/index_html.py b/src/templates/index_html.py new file mode 100644 index 0000000..8ba57fe --- /dev/null +++ b/src/templates/index_html.py @@ -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> +""" diff --git a/src/web.py b/src/web.py new file mode 100644 index 0000000..18b2a83 --- /dev/null +++ b/src/web.py @@ -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 diff --git a/src/wifi.py b/src/wifi.py new file mode 100644 index 0000000..32a9036 --- /dev/null +++ b/src/wifi.py @@ -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()) + + + + + + + +