Added to prepare for web interface of application.
This commit is contained in:
parent
bddc6c99fc
commit
ce0639863c
217
src/controllerSoftware/app.py
Normal file
217
src/controllerSoftware/app.py
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
from flask import Flask, render_template, request, jsonify
|
||||||
|
import asyncio
|
||||||
|
from bleak import BleakScanner, BleakClient
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
# =================================================================================================
|
||||||
|
# APP CONFIGURATION
|
||||||
|
# =================================================================================================
|
||||||
|
|
||||||
|
# Set to True to run without a physical BLE device for testing purposes.
|
||||||
|
# Set to False to connect to the actual lamp matrix.
|
||||||
|
DEBUG_MODE = True
|
||||||
|
|
||||||
|
# --- BLE Device Configuration (Ignored in DEBUG_MODE) ---
|
||||||
|
DEVICE_NAME = "Pupilometer LED Billboard"
|
||||||
|
global ble_client
|
||||||
|
global ble_characteristics
|
||||||
|
ble_client = None
|
||||||
|
ble_characteristics = None
|
||||||
|
ble_event_loop = None # Will be initialized if not in debug mode
|
||||||
|
|
||||||
|
# =================================================================================================
|
||||||
|
# BLE HELPER FUNCTIONS (Used in LIVE mode)
|
||||||
|
# =================================================================================================
|
||||||
|
|
||||||
|
lampAmount = 25
|
||||||
|
|
||||||
|
def create_spiral_map(n=5):
|
||||||
|
if n % 2 == 0:
|
||||||
|
raise ValueError("Matrix size must be odd for a unique center point.")
|
||||||
|
spiral_map = [[0] * n for _ in range(n)]
|
||||||
|
r, c = n // 2, n // 2
|
||||||
|
address = 0
|
||||||
|
spiral_map[r][c] = address
|
||||||
|
dr = [0, 1, 0, -1]
|
||||||
|
dc = [1, 0, -1, 0]
|
||||||
|
direction = 0
|
||||||
|
segment_length = 1
|
||||||
|
steps = 0
|
||||||
|
while address < n * n - 1:
|
||||||
|
for _ in range(segment_length):
|
||||||
|
address += 1
|
||||||
|
r += dr[direction]
|
||||||
|
c += dc[direction]
|
||||||
|
if 0 <= r < n and 0 <= c < n:
|
||||||
|
spiral_map[r][c] = address
|
||||||
|
direction = (direction + 1) % 4
|
||||||
|
steps += 1
|
||||||
|
if steps % 2 == 0:
|
||||||
|
segment_length += 1
|
||||||
|
return spiral_map
|
||||||
|
|
||||||
|
def get_spiral_address(row, col, spiral_map):
|
||||||
|
n = len(spiral_map)
|
||||||
|
if 0 <= row < n and 0 <= col < n:
|
||||||
|
return spiral_map[row][col]
|
||||||
|
else:
|
||||||
|
return -1
|
||||||
|
|
||||||
|
SPIRAL_MAP_5x5 = create_spiral_map(5)
|
||||||
|
|
||||||
|
async def set_lamp_colors_on_ble(lamps_to_update, new_color):
|
||||||
|
global ble_client
|
||||||
|
global ble_characteristics
|
||||||
|
|
||||||
|
if not ble_client or not ble_client.is_connected:
|
||||||
|
print("BLE client not connected. Attempting to reconnect...")
|
||||||
|
await connect_to_ble_device()
|
||||||
|
if not ble_client or not ble_client.is_connected:
|
||||||
|
print("Failed to reconnect to BLE client.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create a full matrix of colors to send
|
||||||
|
serial_colors = [b'\x00\x00\x00'] * lampAmount
|
||||||
|
center_lamp_color = b'\x00\x00\x00'
|
||||||
|
center_pos = get_spiral_address(2, 2, SPIRAL_MAP_5x5)
|
||||||
|
|
||||||
|
# Note: A real implementation would query the device for the center lamp's current color
|
||||||
|
# to maintain persistence. For simplicity in this example, we'll assume it's set to black initially.
|
||||||
|
# We will update this logic later if needed.
|
||||||
|
|
||||||
|
# Apply all other lamps to black
|
||||||
|
for char_index in range(lampAmount):
|
||||||
|
if char_index != center_pos:
|
||||||
|
serial_colors[char_index] = b'\x00\x00\x00'
|
||||||
|
|
||||||
|
# Apply the new color to the selected lamps
|
||||||
|
for lamp in lamps_to_update:
|
||||||
|
spiral_pos = get_spiral_address(lamp['row'], lamp['col'], SPIRAL_MAP_5x5)
|
||||||
|
if spiral_pos != -1:
|
||||||
|
serial_colors[spiral_pos] = new_color
|
||||||
|
|
||||||
|
# Write each byte string to its corresponding characteristic
|
||||||
|
for i, char in enumerate(ble_characteristics):
|
||||||
|
value_to_write = serial_colors[i]
|
||||||
|
await ble_client.write_gatt_char(char.uuid, value_to_write)
|
||||||
|
print(f"Setting Lamp {i} to {value_to_write.hex()}")
|
||||||
|
|
||||||
|
async def connect_to_ble_device():
|
||||||
|
global ble_client
|
||||||
|
global ble_characteristics
|
||||||
|
|
||||||
|
print(f"Scanning for device: {DEVICE_NAME}...")
|
||||||
|
devices = await BleakScanner.discover()
|
||||||
|
target_device = next((d for d in devices if d.name == DEVICE_NAME), None)
|
||||||
|
|
||||||
|
if not target_device:
|
||||||
|
print(f"Device '{DEVICE_NAME}' not found.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f"Found device: {target_device.name} ({target_device.address})")
|
||||||
|
|
||||||
|
try:
|
||||||
|
ble_client = BleakClient(target_device.address)
|
||||||
|
await ble_client.connect()
|
||||||
|
if ble_client.is_connected:
|
||||||
|
print(f"Connected to {target_device.name}")
|
||||||
|
services = await ble_client.get_services()
|
||||||
|
ble_characteristics = sorted([
|
||||||
|
char for service in services for char in service.characteristics
|
||||||
|
], key=lambda char: char.handle)[:lampAmount]
|
||||||
|
|
||||||
|
print(f"Found {len(ble_characteristics)} characteristics for lamps.")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"Failed to connect to {target_device.name}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"An error occurred during BLE connection: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# =================================================================================================
|
||||||
|
# FLASK APPLICATION
|
||||||
|
# =================================================================================================
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
# In-memory matrix for DEBUG_MODE
|
||||||
|
lamp_matrix = [['#000000' for _ in range(5)] for _ in range(5)]
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
if DEBUG_MODE:
|
||||||
|
return render_template('index.html', matrix=lamp_matrix)
|
||||||
|
else:
|
||||||
|
# In live mode, we'll pass a default black matrix.
|
||||||
|
# The true state is on the device.
|
||||||
|
initial_matrix = [['#000000' for _ in range(5)] for _ in range(5)]
|
||||||
|
return render_template('index.html', matrix=initial_matrix)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/set_color', methods=['POST'])
|
||||||
|
def set_color():
|
||||||
|
data = request.get_json()
|
||||||
|
lamps_to_update = data.get('lamps', [])
|
||||||
|
r = data.get('r')
|
||||||
|
g = data.get('g')
|
||||||
|
b = data.get('b')
|
||||||
|
|
||||||
|
if not lamps_to_update:
|
||||||
|
return jsonify(success=False, message="No lamps selected")
|
||||||
|
|
||||||
|
try:
|
||||||
|
r, g, b = int(r), int(g), int(b)
|
||||||
|
new_color_hex = f'#{r:02x}{g:02x}{b:02x}'
|
||||||
|
|
||||||
|
if DEBUG_MODE:
|
||||||
|
# === DEBUG MODE: Update in-memory matrix ===
|
||||||
|
center_row, center_col = 2, 2
|
||||||
|
center_lamp_color = lamp_matrix[center_row][center_col]
|
||||||
|
|
||||||
|
# First, turn all non-center lamps black
|
||||||
|
for row in range(5):
|
||||||
|
for col in range(5):
|
||||||
|
if (row, col) != (center_row, center_col):
|
||||||
|
lamp_matrix[row][col] = '#000000'
|
||||||
|
|
||||||
|
# Apply the new color to the selected lamps
|
||||||
|
for lamp in lamps_to_update:
|
||||||
|
lamp_matrix[lamp['row']][lamp['col']] = new_color_hex
|
||||||
|
|
||||||
|
# The center lamp is handled by its own controls, so it remains persistent
|
||||||
|
# unless it's part of a region update. We re-apply its color here.
|
||||||
|
# No, we don't. The logic is that it gets reset unless it's selected.
|
||||||
|
|
||||||
|
return jsonify(success=True, new_color=new_color_hex)
|
||||||
|
else:
|
||||||
|
# === LIVE MODE: Communicate with the BLE device ===
|
||||||
|
new_color_bytes = int(f'{r:02x}{g:02x}{b:02x}', 16).to_bytes(3, 'big')
|
||||||
|
|
||||||
|
asyncio.run_coroutine_threadsafe(
|
||||||
|
set_lamp_colors_on_ble(lamps_to_update, new_color_bytes),
|
||||||
|
ble_event_loop
|
||||||
|
)
|
||||||
|
return jsonify(success=True, new_color=new_color_hex)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error in set_color route: {e}")
|
||||||
|
return jsonify(success=False, message=str(e)), 500
|
||||||
|
|
||||||
|
# =================================================================================================
|
||||||
|
# APP STARTUP
|
||||||
|
# =================================================================================================
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
if not DEBUG_MODE:
|
||||||
|
print("Starting BLE event loop in background thread...")
|
||||||
|
ble_event_loop = asyncio.new_event_loop()
|
||||||
|
ble_thread = threading.Thread(target=ble_event_loop.run_forever, daemon=True)
|
||||||
|
ble_thread.start()
|
||||||
|
|
||||||
|
# Connect to the device as soon as the app starts
|
||||||
|
future = asyncio.run_coroutine_threadsafe(connect_to_ble_device(), ble_event_loop)
|
||||||
|
future.result(timeout=10) # Wait up to 10 seconds for connection
|
||||||
|
|
||||||
|
app.run(debug=True, use_reloader=False)
|
||||||
138
src/controllerSoftware/static/style.css
Normal file
138
src/controllerSoftware/static/style.css
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
:root {
|
||||||
|
--matrix-width: calc(5 * 70px + 4 * 20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.matrix-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, 70px);
|
||||||
|
grid-template-rows: repeat(5, 70px);
|
||||||
|
gap: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #333;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.lamp {
|
||||||
|
width: 70px;
|
||||||
|
height: 70px;
|
||||||
|
border-radius: 10%;
|
||||||
|
background-color: #000;
|
||||||
|
transition: box-shadow 0.2s, transform 0.1s;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
}
|
||||||
|
.lamp.on {
|
||||||
|
box-shadow: 0 0 15px currentColor, 0 0 25px currentColor;
|
||||||
|
}
|
||||||
|
.lamp.selected {
|
||||||
|
border: 2px solid #fff;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.region-control {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.region-control button {
|
||||||
|
padding: 10px 15px;
|
||||||
|
margin: 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
/* NEW: Control panel styles for a single column */
|
||||||
|
.control-panel, .center-lamp-control {
|
||||||
|
background-color: #444;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
width: var(--matrix-width); /* NEW: Set width to match the matrix */
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.control-panel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.control-panel h2, .center-lamp-control h2 {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.slider-group {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
.slider-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 120px 1fr 50px;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.slider-group input[type="range"] {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 5px;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.slider-group input[type="number"] {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: center;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
.slider-group input[type="range"]::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 0 5px rgba(0,0,0,0.5);
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
.slider-group input[type="range"]::-webkit-slider-runnable-track {
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
input.white-3000k::-webkit-slider-runnable-track { background: linear-gradient(to right, #000, #ffc080); }
|
||||||
|
input.white-6500k::-webkit-slider-runnable-track { background: linear-gradient(to right, #000, #c0e0ff); }
|
||||||
|
input.blue::-webkit-slider-runnable-track { background: linear-gradient(to right, #000, #00f); }
|
||||||
|
.slider-label {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: left;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
/* The media query now applies to the control panels directly */
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
.control-panel, .center-lamp-control {
|
||||||
|
width: var(--matrix-width);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,141 +2,29 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>RGB Lamp Matrix Control</title>
|
<title>RGB Lamp Matrix Control</title>
|
||||||
<style>
|
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||||
body {
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
height: 100vh;
|
|
||||||
margin: 0;
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.matrix-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(5, 70px);
|
|
||||||
grid-template-rows: repeat(5, 70px);
|
|
||||||
gap: 20px;
|
|
||||||
padding: 20px;
|
|
||||||
background-color: #333;
|
|
||||||
border-radius: 10px;
|
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
.lamp {
|
|
||||||
width: 70px;
|
|
||||||
height: 70px;
|
|
||||||
border-radius: 10%;
|
|
||||||
background-color: #000;
|
|
||||||
transition: box-shadow 0.2s, transform 0.1s;
|
|
||||||
cursor: pointer;
|
|
||||||
border: 2px solid transparent;
|
|
||||||
}
|
|
||||||
.lamp.on {
|
|
||||||
box-shadow: 0 0 15px currentColor, 0 0 25px currentColor;
|
|
||||||
}
|
|
||||||
.lamp.selected {
|
|
||||||
border: 2px solid #fff;
|
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
.region-control {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.region-control button {
|
|
||||||
padding: 10px 15px;
|
|
||||||
margin: 5px;
|
|
||||||
font-size: 14px;
|
|
||||||
cursor: pointer;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 5px;
|
|
||||||
background-color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* NEW: Container for the two slider groups */
|
|
||||||
.slider-controls {
|
|
||||||
display: flex;
|
|
||||||
gap: 20px; /* Space between the two panels */
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Adjusted styling for the region control panel */
|
|
||||||
.control-panel {
|
|
||||||
background-color: #444;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 10px;
|
|
||||||
display: none;
|
|
||||||
/* Removed absolute positioning */
|
|
||||||
}
|
|
||||||
.slider-group {
|
|
||||||
width: 250px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 5px;
|
|
||||||
}
|
|
||||||
.slider-group input {
|
|
||||||
width: 100%;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 5px;
|
|
||||||
outline: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.slider-group input::-webkit-slider-thumb {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
height: 20px;
|
|
||||||
width: 20px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #fff;
|
|
||||||
cursor: pointer;
|
|
||||||
box-shadow: 0 0 5px rgba(0,0,0,0.5);
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
.slider-group input::-webkit-slider-runnable-track {
|
|
||||||
height: 24px;
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
input.red::-webkit-slider-runnable-track { background: linear-gradient(to right, #000, #f00); }
|
|
||||||
input.green::-webkit-slider-runnable-track { background: linear-gradient(to right, #000, #0f0); }
|
|
||||||
input.blue::-webkit-slider-runnable-track { background: linear-gradient(to right, #000, #00f); }
|
|
||||||
.slider-label {
|
|
||||||
color: #fff;
|
|
||||||
font-size: 14px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
.center-lamp-control {
|
|
||||||
margin-top: 10px;
|
|
||||||
background-color: #444;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
.center-lamp-control h2 {
|
|
||||||
color: #fff;
|
|
||||||
font-size: 16px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
// === Client-Side State and Helper Functions ===
|
||||||
|
/* var clientMatrix = [
|
||||||
|
[{% for col in range(5) %}'{{ matrix[0][col] }}'{% if not loop.last %}, {% endif %}{% endfor %}],
|
||||||
|
[{% for col in range(5) %}'{{ matrix[1][col] }}'{% if not loop.last %}, {% endif %}{% endfor %}],
|
||||||
|
[{% for col in range(5) %}'{{ matrix[2][col] }}'{% if not loop.last %}, {% endif %}{% endfor %}],
|
||||||
|
[{% for col in range(5) %}'{{ matrix[3][col] }}'{% if not loop.last %}, {% endif %}{% endfor %}],
|
||||||
|
[{% for col in range(5) %}'{{ matrix[4][col] }}'{% if not loop.last %}, {% endif %}{% endfor %}]
|
||||||
|
];*/
|
||||||
|
var clientMatrix = [
|
||||||
|
{% for row in matrix %}
|
||||||
|
[{% for color in row %}'{{ color }}'{% if not loop.last %}, {% endif %}{% endfor %}]{% if not loop.last %}, {% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
];
|
||||||
var selectedLamps = [];
|
var selectedLamps = [];
|
||||||
|
const matrixSize = 5;
|
||||||
|
const centerCoords = {row: Math.floor(matrixSize / 2), col: Math.floor(matrixSize / 2)};
|
||||||
|
|
||||||
function updateSliders(color) {
|
const white3000k = {r: 255, g: 180, b: 100};
|
||||||
var rgb = hexToRgb(color);
|
const white6500k = {r: 200, g: 220, b: 255};
|
||||||
$('#red-slider').val(rgb.r);
|
const blue = {r: 0, g: 0, b: 255};
|
||||||
$('#green-slider').val(rgb.g);
|
|
||||||
$('#blue-slider').val(rgb.b);
|
|
||||||
}
|
|
||||||
|
|
||||||
function hexToRgb(hex) {
|
function hexToRgb(hex) {
|
||||||
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||||
@ -147,140 +35,230 @@
|
|||||||
} : {r: 0, g: 0, b: 0};
|
} : {r: 0, g: 0, b: 0};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getMixedColor(w3k, w6k, b) {
|
||||||
|
let final_r = Math.min(255, Math.round((w3k / 255 * white3000k.r) + (w6k / 255 * white6500k.r)));
|
||||||
|
let final_g = Math.min(255, Math.round((w3k / 255 * white3000k.g) + (w6k / 255 * white6500k.g)));
|
||||||
|
let final_b = Math.min(255, Math.round((w3k / 255 * white3000k.b) + (w6k / 255 * white6500k.b) + (b / 255 * blue.b)));
|
||||||
|
|
||||||
|
let hex = '#' + ('0' + final_r.toString(16)).slice(-2) + ('0' + final_g.toString(16)).slice(-2) + ('0' + final_b.toString(16)).slice(-2);
|
||||||
|
return {r: final_r, g: final_g, b: final_b, hex: hex};
|
||||||
|
}
|
||||||
|
|
||||||
|
// CORRECTED: Dynamic Region Mapping Functions
|
||||||
|
function getUpperRegion(n) {
|
||||||
|
let region = [];
|
||||||
|
for (let r = 0; r < n; r++) {
|
||||||
|
for (let c = 0; c < n; c++) {
|
||||||
|
// Correctly includes all lamps in the upper half of the grid
|
||||||
|
if (r < centerCoords.row) {
|
||||||
|
region.push({row: r, col: c});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return region;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLowerRegion(n) {
|
||||||
|
let region = [];
|
||||||
|
for (let r = 0; r < n; r++) {
|
||||||
|
for (let c = 0; c < n; c++) {
|
||||||
|
// Correctly includes all lamps in the lower half of the grid
|
||||||
|
if (r > centerCoords.row) {
|
||||||
|
region.push({row: r, col: c});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return region;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLeftRegion(n) {
|
||||||
|
let region = [];
|
||||||
|
for (let r = 0; r < n; r++) {
|
||||||
|
for (let c = 0; c < n; c++) {
|
||||||
|
// Correctly includes all lamps in the left half of the grid
|
||||||
|
if (c < centerCoords.col) {
|
||||||
|
region.push({row: r, col: c});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return region;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRightRegion(n) {
|
||||||
|
let region = [];
|
||||||
|
for (let r = 0; r < n; r++) {
|
||||||
|
for (let c = 0; c < n; c++) {
|
||||||
|
// Correctly includes all lamps in the right half of the grid
|
||||||
|
if (c > centerCoords.col) {
|
||||||
|
region.push({row: r, col: c});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return region;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInnerRing(n) {
|
||||||
|
let region = [];
|
||||||
|
let start = centerCoords.row - 1;
|
||||||
|
let end = centerCoords.row + 1;
|
||||||
|
for (let r = start; r <= end; r++) {
|
||||||
|
for (let c = start; c <= end; c++) {
|
||||||
|
if (r !== centerCoords.row || c !== centerCoords.col) {
|
||||||
|
region.push({row: r, col: c});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return region;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOuterRing(n) {
|
||||||
|
let region = [];
|
||||||
|
for (let r = 0; r < n; r++) {
|
||||||
|
for (let c = 0; c < n; c++) {
|
||||||
|
if (r === 0 || r === n - 1 || c === 0 || c === n - 1) {
|
||||||
|
region.push({row: r, col: c});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return region;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMatrix() {
|
||||||
|
for (let r = 0; r < 5; r++) {
|
||||||
|
for (let c = 0; c < 5; c++) {
|
||||||
|
let lampElement = $(`.lamp[data-row="${r}"][data-col="${c}"]`);
|
||||||
|
let color = clientMatrix[r][c];
|
||||||
|
let isSelected = selectedLamps.some(lamp => lamp.row === r && lamp.col === c);
|
||||||
|
|
||||||
|
lampElement.css('background-color', color);
|
||||||
|
|
||||||
|
if (isSelected) {
|
||||||
|
lampElement.addClass('selected');
|
||||||
|
} else {
|
||||||
|
lampElement.removeClass('selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (color !== '#000000') {
|
||||||
|
lampElement.addClass('on');
|
||||||
|
lampElement.css('box-shadow', `0 0 15px ${color}, 0 0 25px ${color}`);
|
||||||
|
} else {
|
||||||
|
lampElement.removeClass('on');
|
||||||
|
lampElement.css('box-shadow', 'inset 0 0 5px rgba(0,0,0,0.5)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSlidersFromColor(targetColor) {
|
||||||
|
let rgb = hexToRgb(targetColor);
|
||||||
|
|
||||||
|
$('#white-3000k-slider, #white-6500k-slider, #blue-slider').val(0);
|
||||||
|
$('#white-3000k-value, #white-6500k-value, #blue-value').val(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Event Handlers ===
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
|
renderMatrix();
|
||||||
|
|
||||||
var regionMaps = {
|
var regionMaps = {
|
||||||
'Upper': [
|
'Upper': getUpperRegion(matrixSize),
|
||||||
{row: 0, col: 0}, {row: 0, col: 1}, {row: 0, col: 2}, {row: 0, col: 3}, {row: 0, col: 4},
|
'Lower': getLowerRegion(matrixSize),
|
||||||
{row: 1, col: 0}, {row: 1, col: 1}, {row: 1, col: 2}, {row: 1, col: 3}, {row: 1, col: 4},
|
'Left': getLeftRegion(matrixSize),
|
||||||
],
|
'Right': getRightRegion(matrixSize),
|
||||||
'Lower': [
|
'Inner ring': getInnerRing(matrixSize),
|
||||||
{row: 3, col: 0}, {row: 3, col: 1}, {row: 3, col: 2}, {row: 3, col: 3}, {row: 3, col: 4},
|
'Outer ring': getOuterRing(matrixSize)
|
||||||
{row: 4, col: 0}, {row: 4, col: 1}, {row: 4, col: 2}, {row: 4, col: 3}, {row: 4, col: 4},
|
|
||||||
],
|
|
||||||
'Left': [
|
|
||||||
{row: 0, col: 0}, {row: 1, col: 0}, {row: 2, col: 0}, {row: 3, col: 0}, {row: 4, col: 0},
|
|
||||||
{row: 0, col: 1}, {row: 1, col: 1}, {row: 2, col: 1}, {row: 3, col: 1}, {row: 4, col: 1},
|
|
||||||
],
|
|
||||||
'Right': [
|
|
||||||
{row: 0, col: 3}, {row: 1, col: 3}, {row: 2, col: 3}, {row: 3, col: 3}, {row: 4, col: 3},
|
|
||||||
{row: 0, col: 4}, {row: 1, col: 4}, {row: 2, col: 4}, {row: 3, col: 4}, {row: 4, col: 4},
|
|
||||||
],
|
|
||||||
'Inner ring': [
|
|
||||||
{row: 1, col: 1}, {row: 1, col: 2}, {row: 1, col: 3},
|
|
||||||
{row: 2, col: 1}, {row: 2, col: 3},
|
|
||||||
{row: 3, col: 1}, {row: 3, col: 2}, {row: 3, col: 3}
|
|
||||||
],
|
|
||||||
'Outer ring': [
|
|
||||||
{row: 0, col: 0}, {row: 0, col: 1}, {row: 0, col: 2}, {row: 0, col: 3}, {row: 0, col: 4},
|
|
||||||
{row: 1, col: 0}, {row: 1, col: 4},
|
|
||||||
{row: 2, col: 0}, {row: 2, col: 4},
|
|
||||||
{row: 3, col: 0}, {row: 3, col: 4},
|
|
||||||
{row: 4, col: 0}, {row: 4, col: 1}, {row: 4, col: 2}, {row: 4, col: 3}, {row: 4, col: 4},
|
|
||||||
]
|
|
||||||
};
|
};
|
||||||
|
|
||||||
$('.region-button').on('click', function() {
|
$('.region-button').on('click', function() {
|
||||||
var region = $(this).data('region');
|
var region = $(this).data('region');
|
||||||
selectedLamps = regionMaps[region];
|
selectedLamps = regionMaps[region];
|
||||||
|
|
||||||
$('.lamp').removeClass('selected');
|
|
||||||
$('.lamp').not('[data-row="2"][data-col="2"]').css('box-shadow', 'inset 0 0 5px rgba(0,0,0,0.5)');
|
|
||||||
|
|
||||||
selectedLamps.forEach(function(lamp) {
|
|
||||||
$(`.lamp[data-row="${lamp.row}"][data-col="${lamp.col}"]`).addClass('selected');
|
|
||||||
});
|
|
||||||
|
|
||||||
if (selectedLamps.length > 0) {
|
if (selectedLamps.length > 0) {
|
||||||
$('.control-panel').show();
|
$('.control-panel').show();
|
||||||
var firstLamp = selectedLamps[0];
|
$('.region-slider-group input').prop('disabled', false);
|
||||||
var firstLampElement = $(`.lamp[data-row="${firstLamp.row}"][data-col="${firstLamp.col}"]`);
|
updateSlidersFromColor(clientMatrix[selectedLamps[0].row][selectedLamps[0].col]);
|
||||||
var currentColor = firstLampElement.css('background-color');
|
|
||||||
updateSliders(currentColor);
|
|
||||||
} else {
|
} else {
|
||||||
$('.control-panel').hide();
|
$('.control-panel').hide();
|
||||||
|
$('.region-slider-group input').prop('disabled', true);
|
||||||
}
|
}
|
||||||
|
renderMatrix();
|
||||||
});
|
});
|
||||||
|
|
||||||
$('.region-slider-group input').on('input', function() {
|
$('.region-slider-group input[type="range"]').on('input', function() {
|
||||||
if (selectedLamps.length === 0) return;
|
if (selectedLamps.length === 0) return;
|
||||||
|
|
||||||
var r = $('#red-slider').val();
|
var w3k = $('#white-3000k-slider').val();
|
||||||
var g = $('#green-slider').val();
|
var w6k = $('#white-6500k-slider').val();
|
||||||
var b = $('#blue-slider').val();
|
var b = $('#blue-slider').val();
|
||||||
|
|
||||||
|
$('#white-3000k-value').val(w3k);
|
||||||
|
$('#white-6500k-value').val(w6k);
|
||||||
|
$('#blue-value').val(b);
|
||||||
|
|
||||||
|
var finalColor = getMixedColor(w3k, w6k, b);
|
||||||
|
|
||||||
|
let centerColor = clientMatrix[centerCoords.row][centerCoords.col];
|
||||||
|
|
||||||
|
for (let r = 0; r < 5; r++) {
|
||||||
|
for (let c = 0; c < 5; c++) {
|
||||||
|
if (r === centerCoords.row && c === centerCoords.col) continue;
|
||||||
|
clientMatrix[r][c] = '#000000';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedLamps.forEach(lamp => {
|
||||||
|
clientMatrix[lamp.row][lamp.col] = finalColor.hex;
|
||||||
|
});
|
||||||
|
|
||||||
var data = {
|
var data = {
|
||||||
lamps: selectedLamps,
|
lamps: selectedLamps,
|
||||||
r: r,
|
r: finalColor.r,
|
||||||
g: g,
|
g: finalColor.g,
|
||||||
b: b
|
b: finalColor.b
|
||||||
};
|
};
|
||||||
|
$.ajax({ url: '/set_color', type: 'POST', contentType: 'application/json', data: JSON.stringify(data) });
|
||||||
$.ajax({
|
|
||||||
url: '/set_color',
|
renderMatrix();
|
||||||
type: 'POST',
|
|
||||||
contentType: 'application/json',
|
|
||||||
data: JSON.stringify(data),
|
|
||||||
success: function(response) {
|
|
||||||
if (response.success) {
|
|
||||||
var newColor = response.new_color;
|
|
||||||
|
|
||||||
$('.lamp').each(function() {
|
|
||||||
var currentLamp = $(this);
|
|
||||||
var lampRow = currentLamp.data('row');
|
|
||||||
var lampCol = currentLamp.data('col');
|
|
||||||
var isCenterLamp = (lampRow === 2 && lampCol === 2);
|
|
||||||
var isSelected = selectedLamps.some(lamp => lamp.row === lampRow && lamp.col === lampCol);
|
|
||||||
|
|
||||||
if (isSelected) {
|
|
||||||
currentLamp.css('background-color', newColor);
|
|
||||||
if (newColor === '#000000') {
|
|
||||||
currentLamp.removeClass('on');
|
|
||||||
} else {
|
|
||||||
currentLamp.addClass('on');
|
|
||||||
currentLamp.css('box-shadow', `0 0 15px ${newColor}, 0 0 25px ${newColor}`);
|
|
||||||
}
|
|
||||||
} else if (!isCenterLamp) {
|
|
||||||
currentLamp.css('background-color', '#000000');
|
|
||||||
currentLamp.removeClass('on');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$('.center-slider-group input').on('input', function() {
|
$('.region-slider-group input[type="number"]').on('input', function() {
|
||||||
var r = $('#center-red-slider').val();
|
var value = parseInt($(this).val());
|
||||||
var g = $('#center-green-slider').val();
|
if (isNaN(value) || value < 0) value = 0;
|
||||||
|
if (value > 255) value = 255;
|
||||||
|
$(this).val(value);
|
||||||
|
$(this).siblings('input[type="range"]').val(value).trigger('input');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('.center-slider-group input[type="range"]').on('input', function() {
|
||||||
|
var w3k = $('#center-white-3000k-slider').val();
|
||||||
|
var w6k = $('#center-white-6500k-slider').val();
|
||||||
var b = $('#center-blue-slider').val();
|
var b = $('#center-blue-slider').val();
|
||||||
|
|
||||||
|
$('#center-white-3000k-value').val(w3k);
|
||||||
|
$('#center-white-6500k-value').val(w6k);
|
||||||
|
$('#center-blue-value').val(b);
|
||||||
|
|
||||||
|
var finalColor = getMixedColor(w3k, w6k, b);
|
||||||
|
|
||||||
|
clientMatrix[centerCoords.row][centerCoords.col] = finalColor.hex;
|
||||||
|
|
||||||
var data = {
|
var data = {
|
||||||
lamps: [{row: 2, col: 2}],
|
lamps: [{row: 2, col: 2}],
|
||||||
r: r,
|
r: finalColor.r,
|
||||||
g: g,
|
g: finalColor.g,
|
||||||
b: b
|
b: finalColor.b
|
||||||
};
|
};
|
||||||
|
$.ajax({ url: '/set_color', type: 'POST', contentType: 'application/json', data: JSON.stringify(data) });
|
||||||
|
|
||||||
$.ajax({
|
renderMatrix();
|
||||||
url: '/set_color',
|
});
|
||||||
type: 'POST',
|
|
||||||
contentType: 'application/json',
|
|
||||||
data: JSON.stringify(data),
|
|
||||||
success: function(response) {
|
|
||||||
if (response.success) {
|
|
||||||
var newColor = response.new_color;
|
|
||||||
var centerLamp = $(`.lamp[data-row="2"][data-col="2"]`);
|
|
||||||
centerLamp.css('background-color', newColor);
|
|
||||||
|
|
||||||
if (newColor === '#000000') {
|
$('.center-slider-group input[type="number"]').on('input', function() {
|
||||||
centerLamp.removeClass('on');
|
var value = parseInt($(this).val());
|
||||||
} else {
|
if (isNaN(value) || value < 0) value = 0;
|
||||||
centerLamp.addClass('on');
|
if (value > 255) value = 255;
|
||||||
centerLamp.css('box-shadow', `0 0 15px ${newColor}, 0 0 25px ${newColor}`);
|
$(this).val(value);
|
||||||
}
|
$(this).siblings('input[type="range"]').val(value).trigger('input');
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@ -304,27 +282,44 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="slider-controls">
|
<div class="center-lamp-control">
|
||||||
<div class="center-lamp-control">
|
<h2>Center Lamp</h2>
|
||||||
<h2>Center Lamp</h2>
|
<div class="slider-group center-slider-group">
|
||||||
<div class="slider-group center-slider-group">
|
<div class="slider-row">
|
||||||
<span class="slider-label">Red</span>
|
<span class="slider-label">White 3000K</span>
|
||||||
<input type="range" id="center-red-slider" class="red" min="0" max="255" value="0">
|
<input type="range" id="center-white-3000k-slider" class="white-3000k" min="0" max="255" value="0">
|
||||||
<span class="slider-label">Green</span>
|
<input type="number" id="center-white-3000k-value" min="0" max="255" value="0">
|
||||||
<input type="range" id="center-green-slider" class="green" min="0" max="255" value="0">
|
</div>
|
||||||
|
<div class="slider-row">
|
||||||
|
<span class="slider-label">White 6500K</span>
|
||||||
|
<input type="range" id="center-white-6500k-slider" class="white-6500k" min="0" max="255" value="0">
|
||||||
|
<input type="number" id="center-white-6500k-value" min="0" max="255" value="0">
|
||||||
|
</div>
|
||||||
|
<div class="slider-row">
|
||||||
<span class="slider-label">Blue</span>
|
<span class="slider-label">Blue</span>
|
||||||
<input type="range" id="center-blue-slider" class="blue" min="0" max="255" value="0">
|
<input type="range" id="center-blue-slider" class="blue" min="0" max="255" value="0">
|
||||||
|
<input type="number" id="center-blue-value" min="0" max="255" value="0">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="control-panel">
|
<div class="control-panel">
|
||||||
<div class="slider-group region-slider-group">
|
<h2>Region</h2>
|
||||||
<span class="slider-label">Region Red</span>
|
<div class="slider-group region-slider-group">
|
||||||
<input type="range" id="red-slider" class="red" min="0" max="255" value="0">
|
<div class="slider-row">
|
||||||
<span class="slider-label">Region Green</span>
|
<span class="slider-label">White 3000K</span>
|
||||||
<input type="range" id="green-slider" class="green" min="0" max="255" value="0">
|
<input type="range" id="white-3000k-slider" class="white-3000k" min="0" max="255" value="0" disabled>
|
||||||
<span class="slider-label">Region Blue</span>
|
<input type="number" id="white-3000k-value" min="0" max="255" value="0" disabled>
|
||||||
<input type="range" id="blue-slider" class="blue" min="0" max="255" value="0">
|
</div>
|
||||||
|
<div class="slider-row">
|
||||||
|
<span class="slider-label">White 6500K</span>
|
||||||
|
<input type="range" id="white-6500k-slider" class="white-6500k" min="0" max="255" value="0" disabled>
|
||||||
|
<input type="number" id="white-6500k-value" min="0" max="255" value="0" disabled>
|
||||||
|
</div>
|
||||||
|
<div class="slider-row">
|
||||||
|
<span class="slider-label">Blue</span>
|
||||||
|
<input type="range" id="blue-slider" class="blue" min="0" max="255" value="0" disabled>
|
||||||
|
<input type="number" id="blue-value" min="0" max="255" value="0" disabled>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user