Repository navigation
Expand file tree
/
Copy pathapp.py
More file actions
175 lines (148 loc) · 6.14 KB
/
Copy pathapp.py
File metadata and controls
175 lines (148 loc) · 6.14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
import uvicorn
import asyncio
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
from pydantic import BaseModel
from lotus_lamp import LotusLamp, DeviceConfig
from contextlib import asynccontextmanager
# --- CONFIGURATION ---
LAMP1_MAC = "BE:16:FF:00:01:A5"
LAMP2_MAC = "BE:16:FF:00:0B:5A"
lamp1 = LotusLamp(device_config=DeviceConfig(name="Light 1", address=LAMP1_MAC))
lamp2 = LotusLamp(device_config=DeviceConfig(name="Light 2", address=LAMP2_MAC))
# Manage Bluetooth connections on startup and shutdown
@asynccontextmanager
async def lifespan(app: FastAPI):
print("Connecting to Stage Lights...")
try:
await lamp1.connect()
print("Light 1 connected.")
except Exception as e:
print(f"Could not connect to Light 1: {e}")
try:
await lamp2.connect()
print("Light 2 connected.")
except Exception as e:
print(f"Could not connect to Light 2: {e}")
yield
print("Shutting down and disconnecting...")
try:
await lamp1.disconnect()
await lamp2.disconnect()
except:
pass
app = FastAPI(lifespan=lifespan)
# --- API DATA MODELS ---
class ColorPayload(BaseModel):
r: int
g: int
b: int
target: str
class PowerPayload(BaseModel):
state: bool
target: str
# --- DEVICE CONTROL LOGIC ---
async def send_command(target: str, command_func, *args):
"""Routes commands to one or both lights concurrently."""
tasks = []
if target in ["1", "both"]:
tasks.append(command_func(lamp1, *args))
if target in ["2", "both"]:
tasks.append(command_func(lamp2, *args))
# Execute together; ignore crashes from one failing light
results = await asyncio.gather(*tasks, return_exceptions=True)
for res in results:
if isinstance(res, Exception):
print(f"Command failed: {res}")
async def do_color(lamp, r, g, b):
await lamp.set_rgb(r, g, b)
async def do_power(lamp, state):
if state:
await lamp.power_on()
else:
await lamp.power_off()
# --- API ENDPOINTS ---
@app.post("/set_color")
async def set_color(payload: ColorPayload):
await send_command(payload.target, do_color, payload.r, payload.g, payload.b)
return {"status": "success"}
@app.post("/set_power")
async def set_power(payload: PowerPayload):
await send_command(payload.target, do_power, payload.state)
return {"status": "success"}
# --- BROWSER FRONTEND ---
HTML_PAGE = """
<!DOCTYPE html>
<html>
<head>
<title>Stage Light Control</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { font-family: -apple-system, sans-serif; background: #121212; color: #fff; text-align: center; padding: 20px; margin: 0; }
.card { background: #1e1e1e; max-width: 400px; margin: 20px auto; padding: 30px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.5); }
h2 { margin-top: 0; }
select { width: 100%; padding: 12px; margin: 15px 0; background: #333; color: white; border: 1px solid #444; border-radius: 8px; font-size: 16px; }
.row { display: flex; justify-content: space-between; align-items: center; margin: 25px 0; text-align: left; }
/* Toggle Switch CSS */
.switch { position: relative; display: inline-block; width: 60px; height: 34px; }
.switch input { opacity: 0; width: 0; height: 0; }
.slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #555; transition: .4s; border-radius: 34px; }
.slider:before { position: absolute; content: ""; height: 26px; width: 26px; left: 4px; bottom: 4px; background-color: white; transition: .4s; border-radius: 50%; }
input:checked + .slider { background-color: #4CAF50; }
input:checked + .slider:before { transform: translateX(26px); }
/* Color Picker CSS */
input[type="color"] { width: 100%; height: 60px; border: none; border-radius: 8px; cursor: pointer; background: none; }
input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; }
input[type="color"]::-webkit-color-swatch { border: none; border-radius: 8px; }
</style>
</head>
<body>
<div class="card">
<h2>StageLights Browser Control</h2>
<label style="display:block; text-align:left; color:#aaa;">Target Device</label>
<select id="targetSelect">
<option value="both">All</option>
<option value="1">Left Light</option>
<option value="2">Right Light</option>
</select>
<div class="row">
<span style="font-size: 18px;">Power</span>
<label class="switch">
<input type="checkbox" id="powerToggle" checked>
<span class="slider"></span>
</label>
</div>
<div style="text-align: left;">
<label style="color:#aaa; display:block; margin-bottom:10px;">Color</label>
<input type="color" id="colorPicker" value="#ffffff">
</div>
</div>
<script>
const targetSelect = document.getElementById('targetSelect');
document.getElementById('powerToggle').addEventListener('change', (e) => {
fetch('/set_power', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ state: e.target.checked, target: targetSelect.value })
});
});
document.getElementById('colorPicker').addEventListener('change', (e) => {
const hex = e.target.value;
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
fetch('/set_color', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ r, g, b, target: targetSelect.value })
});
});
</script>
</body>
</html>
"""
@app.get("/", response_class=HTMLResponse)
async def serve_ui():
return HTMLResponse(content=HTML_PAGE)
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)