Add basic draggable item that saves to the server

This commit is contained in:
Jimmy 2025-07-08 17:39:09 +12:00
parent 5c35e68ab2
commit deca1b6c37
8 changed files with 376 additions and 427 deletions

View File

@ -1,63 +1,23 @@
import asyncio
import aioespnow
from settings import Settings
from patterns import Patterns
from web import web
import gc
import utime
import machine
import ntptime
import time
import wifi
import json
import network
async def main():
settings = Settings()
patterns = Patterns(10, settings["num_leds"], selected=settings["pattern"])
network.WLAN(network.WLAN.IF_STA).active(True)
e = aioespnow.AIOESPNow() # Returns AIOESPNow enhanced with async support
e.active(True)
w = web(settings, patterns)
print(settings)
# start the server in a bacakground task
print("Starting")
w = web(settings)
server = asyncio.create_task(w.start_server(host="0.0.0.0", port=80))
async def send():
peer = bytes.fromhex("e4b323c5411d")
perr = b'\xbb\xbb\xbb\xbb\xbb\xbb'
e.add_peer(peer)
print(peer)
msg = {"delay": 1000}
while True:
if not await e.asend(peer, json.dumps(msg).encode()):
print("Heartbeat: peer not responding:", peer)
else:
print("Heartbeat: ping", peer)
await asyncio.sleep(1)
#asyncio.create_task(send())
wdt = machine.WDT(timeout=10000)
wdt.feed()
while True:
#print(time.localtime())
# gc.collect()
gc.collect()
for i in range(60):
wdt.feed()
await asyncio.sleep_ms(500)
# cleanup before ending the application
await server
asyncio.run(main())

View File

@ -11,14 +11,7 @@ class Settings(dict):
self.load() # Load settings from file during initialization
def set_defaults(self):
self["num_leds"] = 50
self["pattern"] = "on"
self["color1"] = "#00ff00"
self["color2"] = "#ff0000"
self["delay"] = 100
self["brightness"] = 10
self["name"] = "led-controller"
self["ap_password"] = ""
self = {}
def save(self):
try:
@ -39,57 +32,3 @@ class Settings(dict):
print(f"Error loading settings")
self.set_defaults()
self.save()
# 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)
def set_settings(raw_json, settings, patterns):
try:
data = json.loads(raw_json)
print(data)
for key, value in data.items():
print(key, value)
if key == "color1":
patterns.set_color1(tuple(int(value[i:i+2], 16) for i in (1, 3, 5))) # Convert hex to RGB
elif key == "color2":
patterns.set_color2(tuple(int(value[i:i+2], 16) for i in (1, 3, 5))) # Convert hex to RGB
elif key == "num_leds":
patterns.update_num_leds(4, value)
elif key == "pattern":
if not patterns.select(value):
return "Pattern doesn't exist", 400
elif key == "delay":
delay = int(data["delay"])
patterns.set_delay(delay)
elif key == "brightness":
brightness = int(data["brightness"])
patterns.set_brightness(brightness)
elif key == "name":
settings[key] = value
settings.save()
machine.reset()
elif key == "sync":
patterns.sync()
return "OK", 200
else:
return "Invalid key", 400
settings[key] = value
settings.save()
return "OK", 200
except (KeyError, ValueError):
return "Bad request", 400
# Run the example
if __name__ == "__main__":
main()

View File

@ -0,0 +1,143 @@
import { getWebSocket } from "./websocket.js";
export class LightComponent extends HTMLElement {
constructor() {
super();
// Create a shadow DOM for encapsulation
const shadow = this.attachShadow({ mode: "open" });
// Create the content for the component
const style = document.createElement("style");
style.textContent = `
:host {
display: block;
width: 100px;
height: 100px;
background-color: #4caf50;
color: white;
text-align: center;
line-height: 100px;
cursor: grab;
position: absolute;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
:host:active {
cursor: grabbing;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
}
.color-picker {
position: absolute;
top: 0;
right: 0;
width: 50px;
height: 50px;
cursor: pointer;
}
`;
// Create the main content (draggable area)
const content = document.createElement("div");
content.textContent = this.textContent || "Light Me Up!";
content.style.position = "absolute";
content.style.top = "0";
content.style.left = "0";
content.style.width = "100%";
content.style.height = "100%";
content.style.display = "flex";
content.style.justifyContent = "center";
content.style.alignItems = "center";
// Create the color picker
const colorPicker = document.createElement("input");
colorPicker.type = "color";
colorPicker.classList.add("color-picker");
colorPicker.value = "#4caf50"; // Default color
colorPicker.addEventListener("input", () => {
this.style.backgroundColor = colorPicker.value;
this.dispatchEvent(
new CustomEvent("color-change", {
detail: { lightId: this.lightId, color: colorPicker.value },
}),
);
});
// Append the style, content, and color picker to the shadow DOM
shadow.appendChild(style);
shadow.appendChild(content);
shadow.appendChild(colorPicker);
// Add event listeners for drag-and-drop
content.addEventListener("mousedown", this.handleMouseDown.bind(this));
document.addEventListener("mousemove", this.handleMouseMove.bind(this));
document.addEventListener("mouseup", this.handleMouseUp.bind(this));
}
// Track the initial mouse position and component position
handleMouseDown(event) {
event.preventDefault();
// Get the initial mouse position relative to the component
this.initialMouseX = event.clientX;
this.initialMouseY = event.clientY;
// Get the initial position of the component
const rect = this.getBoundingClientRect();
this.initialComponentX = rect.left;
this.initialComponentY = rect.top;
// Add a class to indicate dragging
this.classList.add("dragging");
}
// Update the component's position as the mouse moves
handleMouseMove(event) {
if (!this.classList.contains("dragging")) return;
// Calculate the new position of the component
const newX = this.initialComponentX + (event.clientX - this.initialMouseX);
const newY = this.initialComponentY + (event.clientY - this.initialMouseY);
// Update the component's position
this.style.left = `${newX}px`;
this.style.top = `${newY}px`;
}
// Stop dragging when the mouse is released
handleMouseUp() {
// Check if the component is being dragged
if (!this.classList.contains("dragging")) {
return; // Do nothing if not dragging
}
// Remove the dragging class
this.classList.remove("dragging");
// Get the current position of the component
const rect = this.getBoundingClientRect();
const newX = rect.left;
const newY = rect.top;
// Dispatch an event to notify the parent about the updated position
this.dispatchEvent(
new CustomEvent("position-change", {
detail: { lightId: this.lightId, x: newX, y: newY },
}),
);
}
// Add a property to hold the lightId
set lightId(id) {
this._lightId = id;
}
get lightId() {
return this._lightId;
}
}
// Define the custom element
customElements.define("light-component", LightComponent);

View File

@ -1,214 +1,105 @@
let delayTimeout;
let brightnessTimeout;
let colorTimeout;
let color2Timeout;
let ws; // Variable to hold the WebSocket connection
let connectionStatusElement; // Variable to hold the connection status element
// Import the LightComponent from light-component.js
import { LightComponent } from "./light-component.js";
import { getWebSocket } from "./websocket.js";
// Function to update the connection status indicator
function updateConnectionStatus(status) {
if (!connectionStatusElement) {
connectionStatusElement = document.getElementById("connection-status");
}
if (connectionStatusElement) {
connectionStatusElement.className = ""; // Clear existing classes
connectionStatusElement.classList.add(status);
// Optionally, you could also update text content based on status
// connectionStatusElement.textContent = status.charAt(0).toUpperCase() + status.slice(1);
}
}
// Wait for the DOM to be fully loaded
document.addEventListener("DOMContentLoaded", async () => {
// Select the container where the light-components will be added
const appContainer = document.getElementById("app");
// Function to establish WebSocket connection
function connectWebSocket() {
// Determine the WebSocket URL based on the current location
const wsUrl = `ws://${window.location.host}/ws`;
ws = new WebSocket(wsUrl);
updateConnectionStatus("connecting"); // Indicate connecting state
ws.onopen = function (event) {
console.log("WebSocket connection opened:", event);
updateConnectionStatus("open"); // Indicate open state
// Optionally, you could send an initial message here
};
ws.onmessage = function (event) {
console.log("WebSocket message received:", event.data);
};
ws.onerror = function (event) {
console.error("WebSocket error:", event);
updateConnectionStatus("closed"); // Indicate error state (treat as closed)
};
ws.onclose = function (event) {
if (event.wasClean) {
console.log(
`WebSocket connection closed cleanly, code=${event.code}, reason=${event.reason}`,
);
updateConnectionStatus("closed"); // Indicate closed state
} else {
console.error("WebSocket connection died");
updateConnectionStatus("closed"); // Indicate closed state
}
// Attempt to reconnect after a delay
setTimeout(connectWebSocket, 1000);
};
}
// Function to send data over WebSocket
function sendWebSocketData(data) {
if (ws && ws.readyState === WebSocket.OPEN) {
console.log("Sending data over WebSocket:", data);
ws.send(JSON.stringify(data));
} else {
console.error("WebSocket is not connected. Cannot send data:", data);
// You might want to queue messages or handle this in a different way
}
}
// Keep the post and get functions for now, they might still be useful
async function post(path, data) {
console.log(`POST to ${path}`, data);
// Fetch the JSON data from the /light endpoint
try {
const response = await fetch(path, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
const response = await fetch("/light");
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
} catch (error) {
console.error("Error during POST request:", error);
}
}
const lightData = await response.json();
async function get(path) {
try {
const response = await fetch(path);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
// Map to store backend IDs and their corresponding components
const componentMap = new Map();
// Function to create and configure a light component
function createLightComponent(data, key) {
const lightComponent = document.createElement("light-component");
lightComponent.style.left = `${data.x}px`; // Set the x position
lightComponent.style.top = `${data.y}px`; // Set the y position
lightComponent.style.backgroundColor = data.settings?.color || "#4caf50"; // Set the background color
lightComponent.textContent = data.name || "Light Me Up!"; // Set the text content
// Set the lightId property
lightComponent.lightId = key; // Use the backend ID as the lightId
// Store the component in the map
componentMap.set(key, lightComponent);
// Append the light component to the container
appContainer.appendChild(lightComponent);
// Handle position change
lightComponent.addEventListener("position-change", (event) => {
const { lightId, x, y } = event.detail;
updatePositionOnServer(lightId, x, y);
});
// Handle color change
lightComponent.addEventListener("color-change", (event) => {
const { lightId, color } = event.detail;
sendColorToServer(lightId, color);
});
// Example: Add a click event listener to the light-component
lightComponent.addEventListener("click", () => {
console.log(`Light component clicked! ID: ${lightComponent.lightId}`);
});
}
// Iterate over the JSON data and create light components
for (const key in lightData) {
if (lightData.hasOwnProperty(key)) {
const light = lightData[key];
createLightComponent(light, key); // Pass the backend ID
}
}
// Function to send the updated position to the server via a PATCH request
async function updatePositionOnServer(componentId, x, y) {
try {
const response = await fetch(`/light/${componentId}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ x, y }),
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
console.log(
`Updated position for component ${componentId}: x=${x}, y=${y}`,
);
} catch (error) {
console.error("Error updating position on server:", error);
}
}
// Function to send the selected color to the server via WebSocket
function sendColorToServer(componentId, color) {
const websocket = getWebSocket();
const message = JSON.stringify({
componentId,
color: color,
});
if (websocket.readyState === WebSocket.OPEN) {
websocket.send(message);
console.log("Sent color to server:", message);
} else {
console.warn("WebSocket is not open. Unable to send color.");
}
}
return await response.json();
} catch (error) {
console.error("Error during GET request:", error);
console.error("Error fetching light data:", error);
}
}
async function updateColor(event) {
event.preventDefault();
clearTimeout(colorTimeout);
colorTimeout = setTimeout(function () {
const color = document.getElementById("color").value;
sendWebSocketData({ color1: color });
}, 500);
}
async function updateColor2(event) {
event.preventDefault();
clearTimeout(color2Timeout);
color2Timeout = setTimeout(function () {
const color = document.getElementById("color2").value;
sendWebSocketData({ color2: color });
}, 500);
}
async function updatePattern(pattern) {
sendWebSocketData({ pattern: pattern });
}
async function updateBrightness(event) {
event.preventDefault();
clearTimeout(brightnessTimeout);
brightnessTimeout = setTimeout(function () {
const brightness = document.getElementById("brightness").value;
sendWebSocketData({ brightness: brightness });
}, 500);
}
async function updateDelay(event) {
event.preventDefault();
clearTimeout(delayTimeout);
delayTimeout = setTimeout(function () {
const delay = document.getElementById("delay").value;
sendWebSocketData({ delay: delay });
}, 500);
}
async function updateNumLeds(event) {
event.preventDefault();
const numLeds = document.getElementById("num_leds").value;
sendWebSocketData({ num_leds: parseInt(numLeds) });
}
async function updateName(event) {
event.preventDefault();
const name = document.getElementById("name").value;
sendWebSocketData({ name: name });
}
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";
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 () {
// Get the connection status element once the DOM is ready
connectionStatusElement = document.getElementById("connection-status");
// Establish WebSocket connection on page load
connectWebSocket();
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("name_form").addEventListener("submit", updateName);
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);
});
});
});
// 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";
}

20
src/static/styles.css Normal file
View File

@ -0,0 +1,20 @@
/* Default styles for the light component */
light-component {
display: block;
width: 100px;
height: 100px;
background-color: #4caf50;
color: white;
text-align: center;
line-height: 100px;
cursor: grab;
position: absolute;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
/* Styles when the component is being dragged */
light-component:active {
cursor: grabbing;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
}

26
src/static/websocket.js Normal file
View File

@ -0,0 +1,26 @@
// websocket.js
let websocket = null;
export function getWebSocket() {
if (!websocket) {
// Replace 'ws://your-server-url' with your WebSocket server URL
websocket = new WebSocket(`ws://${window.location.host}/ws`);
// Handle WebSocket connection open
websocket.onopen = () => {
console.log("WebSocket connection established");
};
// Handle WebSocket connection close
websocket.onclose = () => {
console.log("WebSocket connection closed");
};
// Handle WebSocket errors
websocket.onerror = (error) => {
console.error("WebSocket error:", error);
};
}
return websocket;
}

View File

@ -1,97 +1,16 @@
{% args settings, patterns, mac %}
<!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" />
<title>Light Component</title>
<!-- Link to the external CSS file -->
<link rel="stylesheet" href="static/styles.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>
<form id="name_form" method="post" action="/name">
<label for="name">Name:</label>
<input
type="text"
id="name"
name="num_leds"
value="{{settings['name']}}"
/>
<input type="submit" value="Update Name" />
</form>
<!-- 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>
<p>Mac address: {{mac}}</p>
</div>
<div id="connection-status"></div>
<!-- The light-component will be added dynamically by main.js -->
<div id="app"></div>
<!-- Import the JavaScript files -->
<script type="module" src="static/main.js"></script>
</body>
</html>

View File

@ -1,26 +1,15 @@
from microdot import Microdot, send_file, Response
from microdot.utemplate import Template
from microdot.websocket import with_websocket
import machine
from settings import set_settings
import wifi
import json
import aioespnow
def web(settings, patterns):
def web(settings):
app = Microdot()
Response.default_content_type = 'text/html'
e = aioespnow.AIOESPNow() # Returns AIOESPNow enhanced with async support
e.active(True)
peer = bytes.fromhex("e4b323c5411d")
peer = b'\xbb\xbb\xbb\xbb\xbb\xbb'
e.add_peer(peer)
print(peer)
@app.route('/')
async def index_hnadler(request):
mac = wifi.get_mac().hex()
return Template('/index.html').render(settings=settings, patterns=patterns.patterns.keys(), mac=mac)
async def index_handler(request):
return Template('/index.html').render(settings=settings)
@app.route("/static/<path:path>")
def static_handler(request, path):
@ -29,34 +18,96 @@ def web(settings, patterns):
return 'Not found', 404
return send_file('static/' + path)
@app.post("/settings")
def settings_handler(request):
# Keep the POST handler for compatibility or alternative usage if needed
# For WebSocket updates, the /ws handler is now primary
return set_settings(request.body.decode('utf-8'), settings, patterns)
@app.route("/ws")
@with_websocket
async def ws(request, ws):
# Register the client's WebSocket connection
print("WebSocket connection established")
while True:
data = await ws.receive()
if data:
# Process the received data
await send(data)
_, status_code = set_settings(data, settings, patterns)
#await ws.send(status_code)
try:
# Parse the JSON message from the client
message = json.loads(data)
light = message.get("light")
if message["light"] in settings.get("lights"):
settings["lights"][light].update(message["settings"])
if message["save"]:
settings.save()
except json.JSONDecodeError:
print("Invalid JSON received")
else:
break
async def send(data):
print("WebSocket connection closed")
msg = {"delay": 1000}
@app.get("/light")
async def get_lights(request):
return json.dumps(settings)
@app.get("/light/<light>")
async def get_light(request, light):
light_data = settings.get(light, None)
if light_data:
return json.dumps(light_data), 200
else:
return json.dumps({"error": "Light not found"}), 404
@app.post("/light/<light>")
async def add_light(request, light):
try:
if not await e.asend(peer, data):
print("Heartbeat: peer not responding:", peer)
else:
print("Heartbeat: ping", peer)
except:
return
# Parse the JSON request body
data = request.json
# Check if the light already exists
if light in settings:
return json.dumps({"error": "Light already exists"}), 409
# Add the new light to the settings
settings[light] = data
print(settings)
settings.save()
return json.dumps(
{"message": "Light added successfully",
"light": light}
), 200
except Exception as e:
print(f"Exception: {e}")
return json.dumps({"error": "Invalid JSON request"}), 400
@app.patch("/light/<light>")
async def update_light(request, light):
try:
# Parse the JSON request body
data = request.json
print(light, data)
# Check if the light exists
if light not in settings:
return json.dumps({"error": "Light not found"}), 404
# Update the existing light with the provided data
settings[light].update(data)
settings.save() # Uncomment if using persistent storage
return json.dumps(
{"message": "Light updated successfully", "light": settings[light]}
), 200
except json.JSONDecodeError:
return json.dumps({"error": "Invalid JSON request"}), 400
@app.delete("/light/<light>")
async def del_light(request, light):
if light in settings:
# Remove the light from the settings
del settings[light]
settings.save()
return json.dumps({"message": "Light deleted successfully"})
else:
return json.dumps({"error": "Light not found"}), 404
return app
# Example usage
if __name__ == "__main__":
app = web(settings)
app.run()