diff --git a/src/controllerSoftware/app.py b/src/controllerSoftware/app.py new file mode 100644 index 0000000..4512787 --- /dev/null +++ b/src/controllerSoftware/app.py @@ -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) \ No newline at end of file diff --git a/src/controllerSoftware/static/style.css b/src/controllerSoftware/static/style.css new file mode 100644 index 0000000..125bf58 --- /dev/null +++ b/src/controllerSoftware/static/style.css @@ -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); + } +} \ No newline at end of file diff --git a/src/controllerSoftware/templates/index.html b/src/controllerSoftware/templates/index.html index 93716a1..26e1708 100644 --- a/src/controllerSoftware/templates/index.html +++ b/src/controllerSoftware/templates/index.html @@ -2,141 +2,29 @@