diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..5c8e702c --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# Virtual Environment +.venv/ + +# Python cache +__pycache__/ +*.pyc + +# Test artifacts +app_stdout.log +app_stderr.log +screenshots/ diff --git a/README.md b/README.md index 7363a28c..05fdf043 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,28 @@ -### Pupilometer - -## Introduction - -This repository houses programs and documents related to Pupilometer project by Vietnam Academy of Science and Technology. The project aims to introduce a benchmark and researches into the interaction between light intensity and temperature to the eye strain disorder. +### Pupilometer + +## Introduction + +This repository houses programs and documents related to Pupilometer project by Vietnam Academy of Science and Technology. The project aims to introduce a benchmark and researches into the interaction between light intensity and temperature to the eye strain disorder. + +## Dependencies + +### Python Dependencies + +The Python dependencies are listed in the `requirements.txt` file. You can install them using pip: + +```bash +pip install -r requirements.txt +``` + +### NVIDIA DeepStream + +For running the pupil segmentation on a Jetson Orin AGX or a Windows machine with an NVIDIA GPU, this project uses NVIDIA DeepStream. DeepStream is a complex dependency and cannot be installed via pip. + +Please follow the official NVIDIA documentation to install DeepStream for your platform: + +* **Jetson:** [DeepStream for Jetson](https://developer.nvidia.com/deepstream-sdk-jetson) +* **Windows:** [DeepStream for Windows](https://developer.nvidia.com/deepstream-sdk-windows) + +You will also need to install GStreamer and the Python bindings (PyGObject). These are usually installed as part of the DeepStream installation. + +Additionally, the `pyds` library, which provides Python bindings for DeepStream metadata structures, is required. This library is also included with the DeepStream SDK and may need to be installed manually. diff --git a/requirements.txt b/requirements.txt index 51d4ac31..159497f7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,8 @@ -bleak>="1.0.0" -flask>="3.1.1" \ No newline at end of file +bleak>="1.0.0" +flask>="3.1.1" +pypylon>= "4.0.0" +onnxruntime>= "1.18.0" +opencv-python>= "4.9.0" +pytest>= "8.0.0" +pytest-playwright>= "0.4.0" +requests>= "2.31.0" \ No newline at end of file diff --git a/src/controllerSoftware/app.py b/src/controllerSoftware/app.py index aa9be298..e6534f55 100644 --- a/src/controllerSoftware/app.py +++ b/src/controllerSoftware/app.py @@ -1,298 +1,343 @@ -from flask import Flask, render_template, request, jsonify -import asyncio -from bleak import BleakScanner, BleakClient -import threading -import time -import json -import sys -import signal -import os - -# ================================================================================================= -# 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 - - # Updated directions to start moving UP first instead of right - dr = [-1, 0, 1, 0] # Change in row: Up, Right, Down, Left - dc = [0, 1, 0, -1] # Change in col: Up, Right, Down, Left - - 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_full_matrix_on_ble(colorSeries): - 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 - else: - print("Confirmed BLE connection status. Proceeding with lamp update.") - - # ===================================================================== - # SNIPPET TO PATCH SWAPPED LAMP POSITIONS - # ===================================================================== - print("Patching lamp positions 3 <-> 7 and 12 <-> 24.") - - # Swap data for lamps at positions 3 and 7 - temp_color_3 = colorSeries[3] - colorSeries[3] = colorSeries[7] - colorSeries[7] = temp_color_3 - - # Swap data for lamps at positions 12 and 24 - temp_color_12 = colorSeries[12] - colorSeries[12] = colorSeries[24] - colorSeries[24] = temp_color_12 - # ===================================================================== - - if DEBUG_MODE: - # Ensure all characteristics are available before writing - print(f"Confirmed DEBUG set to true.") - if len(ble_characteristics) != lampAmount: - print(f"Mismatch in lamp amount. Expected {lampAmount}, got {len(ble_characteristics)}.") - return - print(f"Constructed the following matrix data: {colorSeries}") - # Write each byte string to its corresponding characteristic - for i, char in enumerate(ble_characteristics): - value_to_write = colorSeries[i] - print(f"Setting Lamp {i} ({char.uuid}) to {value_to_write.hex()}") - await ble_client.write_gatt_char(char.uuid, value_to_write) - else: - print(f"Confirmed DEBUG set to false.") - value_to_write = b"".join([color for color in colorSeries]) - print(value_to_write) - print(f"Setting lamps to {value_to_write.hex()}") - await ble_client.write_gatt_char(ble_characteristics[0].uuid, value_to_write) - - -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 = [service for service in ble_client.services if service.handle != 1] - - # The previous logic for filtering services seems incorrect; let's grab all characteristics - characteristics = [ - char for service in services for char in service.characteristics - ] - ble_characteristics = sorted(characteristics, key=lambda char: char.handle) - 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 -# ================================================================================================= -# COLOR MIXING -# ================================================================================================= - -def calculate_rgb(ww, cw, blue): - """ - Calculates the combined RGB color from warm white, cool white, and blue light values. - This function is a Python equivalent of the JavaScript color mixer in index.html. - """ - # Define the RGB components for each light source based on slider track colors - warm_white_r, warm_white_g, warm_white_b = 255, 192, 128 - cool_white_r, cool_white_g, cool_white_b = 192, 224, 255 - blue_r, blue_g, blue_b = 0, 0, 255 - - # Normalize the slider values (0-255) and apply them to the base colors - r = (ww / 255) * warm_white_r + (cw / 255) * cool_white_r + (blue / 255) * blue_r - g = (ww / 255) * warm_white_g + (cw / 255) * cool_white_g + (blue / 255) * blue_g - b = (ww / 255) * warm_white_b + (cw / 255) * cool_white_b + (blue / 255) * blue_b - - # Clamp the values to 255 and convert to integer - r = int(min(255, round(r))) - g = int(min(255, round(g))) - b = int(min(255, round(b))) - - return r, g, b - -def rgb_to_hex(r, g, b): - """ - Converts RGB color values to a hex color string. - """ - # Ensure values are within the valid range (0-255) and are integers - r = int(max(0, min(255, r))) - g = int(max(0, min(255, g))) - b = int(max(0, min(255, b))) - - # Convert each component to a two-digit hexadecimal string - return f'#{r:02x}{g:02x}{b:02x}' - -# ================================================================================================= -# 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(): - print(f"Getting current lamp matrix info: {lamp_matrix}") - if DEBUG_MODE: - return render_template('index.html', matrix=lamp_matrix) - else: - # In live mode, we'll pass a default black matrix. - initial_matrix = [['#000000' for _ in range(5)] for _ in range(5)] - return render_template('index.html', matrix=initial_matrix) - - -@app.route('/set_matrix', methods=['POST']) -def set_matrix(): - data = request.get_json() - full_matrix = data.get('matrix', []) - matrixDataSerialized = [] - if not full_matrix or len(full_matrix) != 5 or len(full_matrix[0]) != 5: - return jsonify(success=False, message="Invalid matrix data received"), 400 - else: - print(f"Received the following matrix data: {full_matrix}") - - #Creating empty byte array - serial_colors = [b'\x00\x00\x00'] * lampAmount - - try: - for row in range(5): - for col in range(5): - lamp_data = full_matrix[row][col] - ww = int(lamp_data['ww']) - cw = int(lamp_data['cw']) - blue = int(lamp_data['blue']) - - #Preparing byte data for control command - color_bytes = bytes([ww, cw, blue]) - spiral_pos = get_spiral_address(row, col, SPIRAL_MAP_5x5) - print(f"Constructed data for {spiral_pos}: {color_bytes}") - if spiral_pos != -1: - serial_colors[spiral_pos] = color_bytes - #Preparing hex color data for frontend - lampColorR, lampColorG, lampColorB = calculate_rgb(ww,cw,blue) - lamp_matrix[row][col] = rgb_to_hex(lampColorR, lampColorG, lampColorB) - if DEBUG_MODE: - # === DEBUG MODE: Update in-memory matrix === - return jsonify(success=True) - else: - # === LIVE MODE: Communicate with the BLE device === - asyncio.run_coroutine_threadsafe( - set_full_matrix_on_ble(serial_colors), - ble_event_loop - ) - return jsonify(success=True) - except Exception as e: - print(f"Error in set_matrix route: {e}") - return jsonify(success=False, message=str(e)), 500 - - print(f"Getting current lamp matrix info: {lamp_matrix}") - -# ================================================================================================= -# APP STARTUP -# ================================================================================================= - -def signal_handler(signum, frame): - print("Received shutdown signal, gracefully shutting down...") - if not DEBUG_MODE and ble_client and ble_client.is_connected: - print("Disconnecting BLE client...") - disconnect_future = asyncio.run_coroutine_threadsafe(ble_client.disconnect(), ble_event_loop) - try: - # Wait for the disconnect to complete with a timeout - disconnect_future.result(timeout=5) - print("BLE client disconnected successfully.") - except Exception as e: - print(f"Error during BLE disconnect: {e}") - - if not DEBUG_MODE and ble_event_loop and ble_event_loop.is_running(): - print("Stopping BLE event loop...") - # Schedule a stop and wait for the thread to finish - ble_event_loop.call_soon_threadsafe(ble_event_loop.stop) - ble_thread.join(timeout=1) - print("BLE event loop stopped.") - - os._exit(0) - -if __name__ == '__main__': - # Register the signal handler before running the app - signal.signal(signal.SIGINT, signal_handler) - signal.signal(signal.SIGTERM, signal_handler) - - 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, host="0.0.0.0") +from flask import Flask, render_template, request, jsonify +import asyncio +from bleak import BleakScanner, BleakClient +import threading +import time +import json +import sys +import signal +import os +from vision import VisionSystem + +# ================================================================================================= +# 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_connection_status = False + +# ================================================================================================= +# 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 + + # Updated directions to start moving UP first instead of right + dr = [-1, 0, 1, 0] # Change in row: Up, Right, Down, Left + dc = [0, 1, 0, -1] # Change in col: Up, Right, Down, Left + + 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_full_matrix_on_ble(colorSeries): + 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 + else: + print("Confirmed BLE connection status. Proceeding with lamp update.") + + # ===================================================================== + # SNIPPET TO PATCH SWAPPED LAMP POSITIONS + # ===================================================================== + print("Patching lamp positions 3 <-> 7 and 12 <-> 24.") + + # Swap data for lamps at positions 3 and 7 + temp_color_3 = colorSeries[3] + colorSeries[3] = colorSeries[7] + colorSeries[7] = temp_color_3 + + # Swap data for lamps at positions 12 and 24 + temp_color_12 = colorSeries[12] + colorSeries[12] = colorSeries[24] + colorSeries[24] = temp_color_12 + # ===================================================================== + + if DEBUG_MODE: + # Ensure all characteristics are available before writing + print(f"Confirmed DEBUG set to true.") + if len(ble_characteristics) != lampAmount: + print(f"Mismatch in lamp amount. Expected {lampAmount}, got {len(ble_characteristics)}.") + return + print(f"Constructed the following matrix data: {colorSeries}") + # Write each byte string to its corresponding characteristic + for i, char in enumerate(ble_characteristics): + value_to_write = colorSeries[i] + print(f"Setting Lamp {i} ({char.uuid}) to {value_to_write.hex()}") + await ble_client.write_gatt_char(char.uuid, value_to_write) + else: + print(f"Confirmed DEBUG set to false.") + value_to_write = b"".join([color for color in colorSeries]) + print(value_to_write) + print(f"Setting lamps to {value_to_write.hex()}") + await ble_client.write_gatt_char(ble_characteristics[0].uuid, value_to_write) + + +async def connect_to_ble_device(): + global ble_client + global ble_characteristics + global ble_connection_status + + 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.") + ble_connection_status = False + 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 = [service for service in ble_client.services if service.handle != 1] + + # The previous logic for filtering services seems incorrect; let's grab all characteristics + characteristics = [ + char for service in services for char in service.characteristics + ] + ble_characteristics = sorted(characteristics, key=lambda char: char.handle) + print(f"Found {len(ble_characteristics)} characteristics for lamps.") + ble_connection_status = True + return True + else: + print(f"Failed to connect to {target_device.name}") + ble_connection_status = False + return False + except Exception as e: + print(f"An error occurred during BLE connection: {e}") + ble_connection_status = False + return False +# ================================================================================================= +# COLOR MIXING +# ================================================================================================= + +def calculate_rgb(ww, cw, blue): + """ + Calculates the combined RGB color from warm white, cool white, and blue light values. + This function is a Python equivalent of the JavaScript color mixer in index.html. + """ + # Define the RGB components for each light source based on slider track colors + warm_white_r, warm_white_g, warm_white_b = 255, 192, 128 + cool_white_r, cool_white_g, cool_white_b = 192, 224, 255 + blue_r, blue_g, blue_b = 0, 0, 255 + + # Normalize the slider values (0-255) and apply them to the base colors + r = (ww / 255) * warm_white_r + (cw / 255) * cool_white_r + (blue / 255) * blue_r + g = (ww / 255) * warm_white_g + (cw / 255) * cool_white_g + (blue / 255) * blue_g + b = (ww / 255) * warm_white_b + (cw / 255) * cool_white_b + (blue / 255) * blue_b + + # Clamp the values to 255 and convert to integer + r = int(min(255, round(r))) + g = int(min(255, round(g))) + b = int(min(255, round(b))) + + return r, g, b + +def rgb_to_hex(r, g, b): + """ + Converts RGB color values to a hex color string. + """ + # Ensure values are within the valid range (0-255) and are integers + r = int(max(0, min(255, r))) + g = int(max(0, min(255, g))) + b = int(max(0, min(255, b))) + + # Convert each component to a two-digit hexadecimal string + return f'#{r:02x}{g:02x}{b:02x}' + +# ================================================================================================= +# 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(): + print(f"Getting current lamp matrix info: {lamp_matrix}") + if DEBUG_MODE: + return render_template('index.html', matrix=lamp_matrix) + else: + # In live mode, we'll pass a default black matrix. + initial_matrix = [['#000000' for _ in range(5)] for _ in range(5)] + return render_template('index.html', matrix=initial_matrix) + + +@app.route('/set_matrix', methods=['POST']) +def set_matrix(): + data = request.get_json() + full_matrix = data.get('matrix', []) + matrixDataSerialized = [] + if not full_matrix or len(full_matrix) != 5 or len(full_matrix[0]) != 5: + return jsonify(success=False, message="Invalid matrix data received"), 400 + else: + print(f"Received the following matrix data: {full_matrix}") + + #Creating empty byte array + serial_colors = [b'\x00\x00\x00'] * lampAmount + + try: + for row in range(5): + for col in range(5): + lamp_data = full_matrix[row][col] + ww = int(lamp_data['ww']) + cw = int(lamp_data['cw']) + blue = int(lamp_data['blue']) + + #Preparing byte data for control command + color_bytes = bytes([ww, cw, blue]) + spiral_pos = get_spiral_address(row, col, SPIRAL_MAP_5x5) + print(f"Constructed data for {spiral_pos}: {color_bytes}") + if spiral_pos != -1: + serial_colors[spiral_pos] = color_bytes + #Preparing hex color data for frontend + lampColorR, lampColorG, lampColorB = calculate_rgb(ww,cw,blue) + lamp_matrix[row][col] = rgb_to_hex(lampColorR, lampColorG, lampColorB) + if DEBUG_MODE: + # === DEBUG MODE: Update in-memory matrix === + return jsonify(success=True) + else: + # === LIVE MODE: Communicate with the BLE device === + asyncio.run_coroutine_threadsafe( + set_full_matrix_on_ble(serial_colors), + ble_event_loop + ) + return jsonify(success=True) + except Exception as e: + print(f"Error in set_matrix route: {e}") + return jsonify(success=False, message=str(e)), 500 + + print(f"Getting current lamp matrix info: {lamp_matrix}") + +@app.route('/ble_status') +def ble_status(): + global ble_connection_status + if DEBUG_MODE: + return jsonify(connected=True) + return jsonify(connected=ble_connection_status) + +@app.route('/vision/pupil_data') +def get_pupil_data(): + """ + Endpoint to get the latest pupil segmentation data from the vision system. + """ + if vision_system: + data = vision_system.get_pupil_data() + return jsonify(success=True, data=data) + return jsonify(success=False, message="Vision system not initialized"), 500 + +# ================================================================================================= +# APP STARTUP +# ================================================================================================= + +vision_system = None + +def signal_handler(signum, frame): + print("Received shutdown signal, gracefully shutting down...") + global ble_connection_status + + # Stop the vision system + if vision_system: + print("Stopping vision system...") + vision_system.stop() + print("Vision system stopped.") + + if not DEBUG_MODE and ble_client and ble_client.is_connected: + print("Disconnecting BLE client...") + ble_connection_status = False + disconnect_future = asyncio.run_coroutine_threadsafe(ble_client.disconnect(), ble_event_loop) + try: + # Wait for the disconnect to complete with a timeout + disconnect_future.result(timeout=5) + print("BLE client disconnected successfully.") + except Exception as e: + print(f"Error during BLE disconnect: {e}") + + if not DEBUG_MODE and ble_event_loop and ble_event_loop.is_running(): + print("Stopping BLE event loop...") + # Schedule a stop and wait for the thread to finish + ble_event_loop.call_soon_threadsafe(ble_event_loop.stop) + ble_thread.join(timeout=1) + print("BLE event loop stopped.") + + os._exit(0) + +if __name__ == '__main__': + # Register the signal handler before running the app + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + # Initialize and start the Vision System + try: + vision_config = {"camera_id": 0, "model_path": "yolov10.onnx"} + vision_system = VisionSystem(config=vision_config) + vision_system.start() + except Exception as e: + print(f"Failed to initialize or start Vision System: {e}") + vision_system = None + + + 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, host="0.0.0.0") diff --git a/src/controllerSoftware/calib.bin b/src/controllerSoftware/calib.bin new file mode 100644 index 00000000..e69de29b diff --git a/src/controllerSoftware/deepstream_pipeline.py b/src/controllerSoftware/deepstream_pipeline.py new file mode 100644 index 00000000..3e894d3a --- /dev/null +++ b/src/controllerSoftware/deepstream_pipeline.py @@ -0,0 +1,235 @@ +import sys +import gi +gi.require_version('Gst', '1.0') +from gi.repository import Gst, GLib +import pyds +import threading +try: + from pypylon import pylon +except ImportError: + print("pypylon is not installed. DeepStreamBackend will not be able to get frames from Basler camera.") + pylon = None + +class DeepStreamPipeline: + """ + A class to manage the DeepStream pipeline for pupil segmentation. + """ + + def __init__(self, config): + self.config = config + Gst.init(None) + self.pipeline = None + self.loop = GLib.MainLoop() + self.pupil_data = None + self.camera = None + self.frame_feeder_thread = None + self.is_running = False + print("DeepStreamPipeline initialized.") + + def _frame_feeder_thread(self, appsrc): + """ + Thread function to feed frames from the Basler camera to the appsrc element. + """ + while self.is_running: + if not self.camera or not self.camera.IsGrabbing(): + print("Camera not ready, stopping frame feeder.") + break + + try: + grab_result = self.camera.RetrieveResult(5000, pylon.TimeoutHandling_ThrowException) + if grab_result.GrabSucceeded(): + frame = grab_result.Array + + # Create a Gst.Buffer + buf = Gst.Buffer.new_allocate(None, len(frame), None) + buf.fill(0, frame) + + # Push the buffer into the appsrc + appsrc.emit('push-buffer', buf) + else: + print(f"Error grabbing frame: {grab_result.ErrorCode}") + except Exception as e: + print(f"An error occurred in frame feeder thread: {e}") + break + finally: + if 'grab_result' in locals() and grab_result: + grab_result.Release() + + def bus_call(self, bus, message, loop): + """ + Callback function for handling messages from the GStreamer bus. + """ + t = message.type + if t == Gst.MessageType.EOS: + sys.stdout.write("End-of-stream\n") + self.is_running = False + loop.quit() + elif t == Gst.MessageType.WARNING: + err, debug = message.parse_warning() + sys.stderr.write("Warning: %s: %s\n" % (err, debug)) + elif t == Gst.MessageType.ERROR: + err, debug = message.parse_error() + sys.stderr.write("Error: %s: %s\n" % (err, debug)) + self.is_running = False + loop.quit() + return True + + def pgie_sink_pad_buffer_probe(self, pad, info, u_data): + """ + Probe callback function for the sink pad of the pgie element. + """ + gst_buffer = info.get_buffer() + if not gst_buffer: + print("Unable to get GstBuffer ") + return Gst.PadProbeReturn.OK + + # Retrieve batch metadata from the gst_buffer + # Note that pyds.gst_buffer_get_nvds_batch_meta() expects the address of gst_buffer as input, which is a ptr. + batch_meta = pyds.gst_buffer_get_nvds_batch_meta(hash(gst_buffer)) + l_frame = batch_meta.frame_meta_list + while l_frame is not None: + try: + # Note that l_frame.data needs a cast to pyds.NvDsFrameMeta + frame_meta = pyds.glist_get_data(l_frame) + except StopIteration: + break + + l_obj = frame_meta.obj_meta_list + while l_obj is not None: + try: + # Casting l_obj.data to pyds.NvDsObjectMeta + obj_meta = pyds.glist_get_data(l_obj) + except StopIteration: + break + + # Access and process object metadata + rect_params = obj_meta.rect_params + top = rect_params.top + left = rect_params.left + width = rect_params.width + height = rect_params.height + + self.pupil_data = { + "bounding_box": [left, top, left + width, top + height], + "confidence": obj_meta.confidence + } + + print(f"Pupil detected: {self.pupil_data}") + + try: + l_obj = l_obj.next + except StopIteration: + break + try: + l_frame = l_frame.next + except StopIteration: + break + return Gst.PadProbeReturn.OK + + def start(self): + """ + Builds and starts the DeepStream pipeline. + """ + if not pylon: + raise ImportError("pypylon is not installed. Cannot start DeepStreamPipeline with Basler camera.") + + # Initialize camera + try: + self.camera = pylon.InstantCamera(pylon.TlFactory.GetInstance().CreateFirstDevice()) + self.camera.Open() + self.camera.StartGrabbing(pylon.GrabStrategy_LatestImageOnly) + print("DeepStreamPipeline: Basler camera opened and started grabbing.") + except Exception as e: + print(f"DeepStreamPipeline: Error opening Basler camera: {e}") + return + + self.pipeline = Gst.Pipeline() + if not self.pipeline: + sys.stderr.write(" Unable to create Pipeline \n") + return + + source = Gst.ElementFactory.make("appsrc", "app-source") + # ... (element creation remains the same) + pgie = Gst.ElementFactory.make("nvinfer", "primary-inference") + sink = Gst.ElementFactory.make("fakesink", "sink") + videoconvert = Gst.ElementFactory.make("nvvideoconvert", "nv-videoconvert") + + + # Set appsrc properties + # TODO: Set caps based on camera properties + caps = Gst.Caps.from_string("video/x-raw,format=GRAY8,width=1280,height=720,framerate=30/1") + source.set_property("caps", caps) + source.set_property("format", "time") + + pgie.set_property('config-file-path', "pgie_yolov10_config.txt") + + self.pipeline.add(source) + self.pipeline.add(videoconvert) + self.pipeline.add(pgie) + self.pipeline.add(sink) + + if not source.link(videoconvert): + sys.stderr.write(" Unable to link source to videoconvert \n") + return + if not videoconvert.link(pgie): + sys.stderr.write(" Unable to link videoconvert to pgie \n") + return + if not pgie.link(sink): + sys.stderr.write(" Unable to link pgie to sink \n") + return + + pgie_sink_pad = pgie.get_static_pad("sink") + if not pgie_sink_pad: + sys.stderr.write(" Unable to get sink pad of pgie \n") + return + pgie_sink_pad.add_probe(Gst.PadProbeType.BUFFER, self.pgie_sink_pad_buffer_probe, 0) + + bus = self.pipeline.get_bus() + bus.add_signal_watch() + bus.connect("message", self.bus_call, self.loop) + + self.is_running = True + self.frame_feeder_thread = threading.Thread(target=self._frame_feeder_thread, args=(source,)) + self.frame_feeder_thread.start() + + print("Starting pipeline...") + self.pipeline.set_state(Gst.State.PLAYING) + + print("DeepStreamPipeline started.") + + def stop(self): + """ + Stops the DeepStream pipeline. + """ + self.is_running = False + if self.frame_feeder_thread: + self.frame_feeder_thread.join() + + if self.pipeline: + self.pipeline.set_state(Gst.State.NULL) + print("DeepStreamPipeline stopped.") + + if self.camera and self.camera.IsGrabbing(): + self.camera.StopGrabbing() + if self.camera and self.camera.IsOpen(): + self.camera.Close() + print("DeepStreamPipeline: Basler camera closed.") + + def get_data(self): + """ + Retrieves data from the pipeline. + """ + return self.pupil_data + +if __name__ == '__main__': + config = {} + pipeline = DeepStreamPipeline(config) + pipeline.start() + + # Run the GLib main loop in the main thread + try: + pipeline.loop.run() + except KeyboardInterrupt: + print("Interrupted by user.") + + pipeline.stop() diff --git a/src/controllerSoftware/labels.txt b/src/controllerSoftware/labels.txt new file mode 100644 index 00000000..e69de29b diff --git a/src/controllerSoftware/model.engine b/src/controllerSoftware/model.engine new file mode 100644 index 00000000..e69de29b diff --git a/src/controllerSoftware/pgie_yolov10_config.txt b/src/controllerSoftware/pgie_yolov10_config.txt new file mode 100644 index 00000000..668d4d83 --- /dev/null +++ b/src/controllerSoftware/pgie_yolov10_config.txt @@ -0,0 +1,18 @@ +[property] +gpu-id=0 +net-scale-factor=0.00392156862745098 +#onnx-file=yolov10.onnx +model-engine-file=model.engine +#labelfile-path=labels.txt +batch-size=1 +process-mode=1 +model-color-format=0 +network-mode=0 +num-detected-classes=1 +gie-unique-id=1 +output-blob-names=output0 + +[class-attrs-all] +pre-cluster-threshold=0.2 +eps=0.2 +group-threshold=1 diff --git a/src/controllerSoftware/static/script.js b/src/controllerSoftware/static/script.js new file mode 100644 index 00000000..7a2bc34a --- /dev/null +++ b/src/controllerSoftware/static/script.js @@ -0,0 +1,285 @@ +// State for the entire 5x5 matrix, storing {ww, cw, blue} for each lamp +var lampMatrixState = Array(5).fill(null).map(() => Array(5).fill({ww: 0, cw: 0, blue: 0})); +var selectedLamps = []; + +// Function to calculate a visual RGB color from the three light values using a proper additive model +function calculateRgb(ww, cw, blue) { + // Define the RGB components for each light source based on slider track colors + const warmWhiteR = 255; + const warmWhiteG = 192; + const warmWhiteB = 128; + + const coolWhiteR = 192; + const coolWhiteG = 224; + const coolWhiteB = 255; + + const blueR = 0; + const blueG = 0; + const blueB = 255; + + // Normalize the slider values (0-255) and apply them to the base colors + var r = (ww / 255) * warmWhiteR + (cw / 255) * coolWhiteR + (blue / 255) * blueR; + var g = (ww / 255) * warmWhiteG + (cw / 255) * coolWhiteG + (blue / 255) * blueG; + var b = (ww / 255) * warmWhiteB + (cw / 255) * coolWhiteB + (blue / 255) * blueB; + + // Clamp the values to 255 and convert to integer + r = Math.min(255, Math.round(r)); + g = Math.min(255, Math.round(g)); + b = Math.min(255, Math.round(b)); + + // Convert to hex string + var toHex = (c) => ('0' + c.toString(16)).slice(-2); + return '#' + toHex(r) + toHex(g) + toHex(b); +} + +function updateLampUI(lamp, colorState) { + var newColor = calculateRgb(colorState.ww, colorState.cw, colorState.blue); + var lampElement = $(`.lamp[data-row="${lamp.row}"][data-col="${lamp.col}"]`); + lampElement.css('background-color', newColor); + if (newColor === '#000000') { + lampElement.removeClass('on'); + lampElement.css('box-shadow', `inset 0 0 5px rgba(0,0,0,0.5)`); + } else { + lampElement.addClass('on'); + lampElement.css('box-shadow', `0 0 15px ${newColor}, 0 0 25px ${newColor}`); + } +} + +// Function to update the UI and send the full matrix state to the backend +function sendFullMatrixUpdate(lampsToUpdate, isRegionUpdate = false) { + var fullMatrixData = lampMatrixState.map(row => row.map(lamp => ({ + ww: lamp.ww, + cw: lamp.cw, + blue: lamp.blue + }))); + + $.ajax({ + url: '/set_matrix', + type: 'POST', + contentType: 'application/json', + data: JSON.stringify({ matrix: fullMatrixData }), + success: function(response) { + if (response.success) { + if (isRegionUpdate) { + // On a region button click, update the entire matrix UI + for (var r = 0; r < 5; r++) { + for (var c = 0; c < 5; c++) { + updateLampUI({row: r, col: c}, lampMatrixState[r][c]); + } + } + } else { + // Otherwise, just update the lamps that changed + lampsToUpdate.forEach(function(lamp) { + updateLampUI(lamp, lampMatrixState[lamp.row][lamp.col]); + }); + } + } + } + }); +} + +function updateSliders(ww, cw, blue, prefix = '') { + $(`#${prefix}ww-slider`).val(ww); + $(`#${prefix}cw-slider`).val(cw); + $(`#${prefix}blue-slider`).val(blue); + $(`#${prefix}ww-number`).val(ww); + $(`#${prefix}cw-number`).val(cw); + $(`#${prefix}blue-number`).val(blue); +} + +$(document).ready(function() { + var regionMaps = { + 'Upper': [ + {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: 1}, {row: 1, col: 2}, {row: 1, col: 3}, {row: 1, col: 4}, + ], + 'Lower': [ + {row: 3, col: 0}, {row: 3, col: 1}, {row: 3, col: 2}, {row: 3, col: 3}, {row: 3, col: 4}, + {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}, + ], + 'All': [ + {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: 1}, {row: 1, col: 2}, {row: 1, col: 3}, {row: 1, col: 4}, + {row: 2, col: 0}, {row: 2, col: 1}, {row: 2, col: 3}, {row: 2, col: 4}, + {row: 3, col: 0}, {row: 3, col: 1}, {row: 3, col: 2}, {row: 3, col: 3}, {row: 3, col: 4}, + {row: 4, col: 0}, {row: 4, col: 1}, {row: 4, col: 2}, {row: 4, col: 3}, {row: 4, col: 4}, + ] + }; + + // Exclude the center lamp from the 'All' region + var allRegionWithoutCenter = regionMaps['All'].filter(lamp => !(lamp.row === 2 && lamp.col === 2)); + regionMaps['All'] = allRegionWithoutCenter; + + // Initialize lampMatrixState from the initial HTML colors + $('.lamp').each(function() { + var row = $(this).data('row'); + var col = $(this).data('col'); + var color = $(this).css('background-color'); + var rgb = color.match(/\d+/g); + lampMatrixState[row][col] = { + ww: rgb[0], cw: rgb[1], blue: rgb[2] + }; + }); + + $('#region-select').on('change', function() { + var region = $(this).val(); + + // Toggle the inactive state of the control panel based on selection + if (region) { + $('.control-panel').removeClass('inactive-control'); + } else { + $('.control-panel').addClass('inactive-control'); + } + + var newlySelectedLamps = regionMaps[region]; + + // Clear selected class from all lamps + $('.lamp').removeClass('selected'); + + // Get the current slider values to use as the new default + var ww = parseInt($('#ww-slider').val()); + var cw = parseInt($('#cw-slider').val()); + var blue = parseInt($('#blue-slider').val()); + + // Reset all lamps except the center to black in our state + var lampsToUpdate = []; + var centerLampState = lampMatrixState[2][2]; + lampMatrixState = Array(5).fill(null).map(() => Array(5).fill({ww: 0, cw: 0, blue: 0})); + lampMatrixState[2][2] = centerLampState; // Preserve center lamp state + + // Set newly selected lamps to the current slider values + selectedLamps = newlySelectedLamps; + selectedLamps.forEach(function(lamp) { + $(`.lamp[data-row="${lamp.row}"][data-col="${lamp.col}"]`).addClass('selected'); + lampMatrixState[lamp.row][lamp.col] = {ww: ww, cw: cw, blue: blue}; + }); + + if (selectedLamps.length > 0) { + // Update sliders to reflect the state of the first selected lamp + var firstLamp = selectedLamps[0]; + var firstLampState = lampMatrixState[firstLamp.row][firstLamp.col]; + updateSliders(firstLampState.ww, firstLampState.cw, firstLampState.blue, ''); + } + + // Send the full matrix state + sendFullMatrixUpdate(lampsToUpdate, true); + }); + + // Event listener for the region sliders and number inputs + $('.region-slider-group input').on('input', function() { + if (selectedLamps.length === 0) return; + + var target = $(this); + var originalVal = target.val(); + var value = parseInt(originalVal, 10); + + // Clamp value + if (isNaN(value) || value < 0) { value = 0; } + if (value > 255) { value = 255; } + + if (target.is('[type="number"]') && value.toString() !== originalVal) { + target.val(value); + } + + var id = target.attr('id'); + if (target.is('[type="range"]')) { + $(`#${id.replace('-slider', '-number')}`).val(value); + } else if (target.is('[type="number"]')) { + $(`#${id.replace('-number', '-slider')}`).val(value); + } + + var ww = parseInt($('#ww-slider').val()); + var cw = parseInt($('#cw-slider').val()); + var blue = parseInt($('#blue-slider').val()); + + var lampsToUpdate = []; + selectedLamps.forEach(function(lamp) { + lampMatrixState[lamp.row][lamp.col] = {ww: ww, cw: cw, blue: blue}; + lampsToUpdate.push(lamp); + }); + + sendFullMatrixUpdate(lampsToUpdate); + }); + + // Event listener for the center lamp sliders and number inputs + $('.center-slider-group input').on('input', function() { + var target = $(this); + var originalVal = target.val(); + var value = parseInt(originalVal, 10); + + // Clamp value + if (isNaN(value) || value < 0) { value = 0; } + if (value > 255) { value = 255; } + + if (target.is('[type="number"]') && value.toString() !== originalVal) { + target.val(value); + } + + var id = target.attr('id'); + if (target.is('[type="range"]')) { + $(`#${id.replace('-slider', '-number')}`).val(value); + } else if (target.is('[type="number"]')) { + $(`#${id.replace('-number', '-slider')}`).val(value); + } + + var ww = parseInt($('#center-ww-slider').val()); + var cw = parseInt($('#center-cw-slider').val()); + var blue = parseInt($('#center-blue-slider').val()); + + var centerLamp = {row: 2, col: 2}; + + lampMatrixState[centerLamp.row][centerLamp.col] = {ww: ww, cw: cw, blue: blue}; + + sendFullMatrixUpdate([centerLamp]); + }); + + // Initial check to set the inactive state + if (!$('#region-select').val()) { + $('.control-panel').addClass('inactive-control'); + } + + function checkBleStatus() { + $.ajax({ + url: '/ble_status', + type: 'GET', + success: function(response) { + var statusElement = $('#ble-status'); + if (response.connected) { + statusElement.text('BLE Connected'); + statusElement.css('color', 'lightgreen'); + } else { + statusElement.text('BLE Disconnected'); + statusElement.css('color', 'red'); + } + }, + error: function() { + var statusElement = $('#ble-status'); + statusElement.text('Reconnecting...'); + statusElement.css('color', 'orange'); + } + }); + } + + setInterval(checkBleStatus, 2000); + checkBleStatus(); // Initial check +}); \ No newline at end of file diff --git a/src/controllerSoftware/static/style.css b/src/controllerSoftware/static/style.css index 0f5a0518..24220f44 100644 --- a/src/controllerSoftware/static/style.css +++ b/src/controllerSoftware/static/style.css @@ -1,151 +1,162 @@ -:root { - --matrix-width: calc(5 * 70px + 4 * 20px); -} - -body { - font-family: Arial, sans-serif; - display: flex; - flex-direction: column; - align-items: center; - margin: 0; - background-color: #f0f0f0; - min-height: 100vh; -} -.container { - display: flex; - flex-direction: column; - align-items: center; - position: relative; -} -.main-content { - display: flex; - flex-direction: row; - align-items: flex-start; - gap: 40px; -} -.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 select { - padding: 10px 15px; - font-size: 14px; - cursor: pointer; - border: 1px solid #ccc; - border-radius: 5px; - background-color: #fff; - width: 200px; -} -.control-panel, .center-lamp-control { - background-color: #444; - padding: 20px; - border-radius: 10px; - width: var(--matrix-width); /* Fixed width for consistency */ - max-width: var(--matrix-width); - margin-bottom: 20px; -} -.control-panel.inactive-control { - background-color: #333; - filter: saturate(0.2); -} -.control-panel.inactive-control .slider-row { - pointer-events: 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: 150px 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; -} -.inactive-control .slider-label { - color: #888; -} - -@media (max-width: 1000px) { - .main-content { - flex-direction: column; - align-items: center; - } +:root { + --matrix-width: calc(5 * 70px + 4 * 20px); +} + +body { + font-family: Arial, sans-serif; + display: flex; + flex-direction: column; + align-items: center; + margin: 0; + background-color: #f0f0f0; + min-height: 100vh; +} +.container { + display: flex; + flex-direction: column; + align-items: center; + position: relative; +} +.main-content { + display: flex; + flex-direction: row; + align-items: flex-start; + gap: 40px; +} +.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 select { + padding: 10px 15px; + font-size: 14px; + cursor: pointer; + border: 1px solid #ccc; + border-radius: 5px; + background-color: #fff; + width: 200px; +} +.control-panel, .center-lamp-control { + background-color: #444; + padding: 20px; + border-radius: 10px; + width: var(--matrix-width); /* Fixed width for consistency */ + max-width: var(--matrix-width); + margin-bottom: 20px; +} +.control-panel.inactive-control { + background-color: #333; + filter: saturate(0.2); +} +.control-panel.inactive-control .slider-row { + pointer-events: 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: 150px 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; +} +.inactive-control .slider-label { + color: #888; +} + +@media (max-width: 1000px) { + .main-content { + flex-direction: column; + align-items: center; + } +} + +#ble-status { + position: fixed; + top: 10px; + right: 10px; + font-size: 16px; + color: #fff; + background-color: #333; + padding: 5px 10px; + border-radius: 5px; } \ No newline at end of file diff --git a/src/controllerSoftware/templates/index.html b/src/controllerSoftware/templates/index.html index a1efb11b..11c0daa7 100644 --- a/src/controllerSoftware/templates/index.html +++ b/src/controllerSoftware/templates/index.html @@ -1,342 +1,82 @@ - - -
-