Created
March 7, 2025 19:59
-
-
Save parkerlreed/348ebc2ed35137281e1bc0d78af8fc6f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
import sys | |
import bluepy.btle as btle | |
import crcmod | |
from PyQt6.QtWidgets import ( | |
QApplication, QWidget, QPushButton, QVBoxLayout, QSlider, QLabel, QColorDialog | |
) | |
from PyQt6.QtCore import Qt | |
from PyQt6.QtGui import QColor | |
# Bluetooth device MAC address | |
DEVICE_MAC = "FC:58:FA:46:B1:77" | |
# Define CRC-16 function using crcmod | |
def calculate_crc(data): | |
crc16_func = crcmod.predefined.Crc("modbus") | |
crc16_func.update(data) | |
return crc16_func.digest()[::-1] # Reverse bytes for little-endian | |
# Global BLE connection | |
ble_connection = None | |
ble_characteristic = None # Store characteristic for faster writes | |
def connect_to_device(): | |
"""Establish a persistent BLE connection.""" | |
global ble_connection, ble_characteristic | |
if ble_connection is None: | |
try: | |
print("Connecting to BLE device...") | |
ble_connection = btle.Peripheral(DEVICE_MAC) | |
service = ble_connection.getServiceByUUID("0000ffa0-0000-1000-8000-00805f9b34fb") | |
ble_characteristic = service.getCharacteristics()[0] # Store characteristic reference | |
print("Connected.") | |
except Exception as e: | |
print(f"Failed to connect: {e}") | |
sys.exit(1) | |
def send_command(command): | |
"""Sends a BLE command using the persistent connection.""" | |
global ble_connection, ble_characteristic | |
try: | |
if ble_connection is None or ble_characteristic is None: | |
connect_to_device() | |
ble_characteristic.write(command, withResponse=False) # Fast write | |
except Exception as e: | |
print(f"Error sending command: {e}") | |
ble_connection = None # Reset connection in case of failure | |
def set_color(r, g, b): | |
"""Sends an RGB color command to the BLE device.""" | |
color_bytes = bytes([r, g, b]) | |
command = b'\xaa\x17\x0a' + color_bytes + color_bytes + b'\x64' | |
crc16 = calculate_crc(command) | |
send_command(command + crc16) | |
def set_brightness(value): | |
"""Sends brightness level to the BLE device.""" | |
command = b'\xaa\x13\x04' + bytes([value]) | |
crc16 = calculate_crc(command) | |
send_command(command + crc16) | |
def map_white_value(level): | |
"""Maps 0-255 to the correct 2-byte sequence for white temperature control.""" | |
warm_bytes = b'\xae\x54' # Full warm | |
cold_bytes = b'\xf9\xfb' # Full cold | |
warm_int = int.from_bytes(warm_bytes, 'big') | |
cold_int = int.from_bytes(cold_bytes, 'big') | |
mapped_value = int(warm_int + ((level / 255) * (cold_int - warm_int))) | |
return mapped_value.to_bytes(2, 'big') | |
def set_white(level): | |
"""Sends white level command to the BLE device.""" | |
white_bytes = map_white_value(level) | |
control_byte = b'\x00' if level == 0 else b'\xff' if level == 255 else bytes([int((level / 255) * 255)]) | |
command = b'\xaa\x20\x07\xff' + white_bytes + control_byte | |
crc16 = calculate_crc(command) | |
send_command(command + crc16) | |
def turn_off(): | |
"""Turns off both white and color.""" | |
print("Turning off...") | |
command_off = b'\xaa\x11\x04\x00\x73\x39' # White & color off | |
crc_off = calculate_crc(command_off) | |
send_command(command_off + crc_off) | |
# PyQt6 UI Class | |
class LightControlUI(QWidget): | |
def __init__(self): | |
super().__init__() | |
# Set up UI layout | |
self.setWindowTitle("BLE Light Controller") | |
self.setGeometry(100, 100, 450, 350) | |
layout = QVBoxLayout() | |
# Embedded Color Picker | |
self.color_label = QLabel("Select Color") | |
layout.addWidget(self.color_label) | |
self.color_picker = QColorDialog(self) | |
self.color_picker.setOption(QColorDialog.ColorDialogOption.NoButtons) # Removes OK/Cancel | |
self.color_picker.setOption(QColorDialog.ColorDialogOption.ShowAlphaChannel, False) # No transparency | |
self.color_picker.setWindowFlags(Qt.WindowType.Widget) # Fully embeds it | |
# **No extra styling applied – keeps full native look** | |
self.color_picker.currentColorChanged.connect(self.pick_color) # Live update on selection | |
layout.addWidget(self.color_picker) | |
# White Level Slider | |
self.white_label = QLabel("White Level (Warm ↔ Cold)") | |
self.white_slider = QSlider(Qt.Orientation.Horizontal) | |
self.white_slider.setMinimum(0) | |
self.white_slider.setMaximum(255) | |
self.white_slider.setValue(128) # Default middle | |
self.white_slider.valueChanged.connect(self.update_white) | |
layout.addWidget(self.white_label) | |
layout.addWidget(self.white_slider) | |
# Brightness Slider | |
self.brightness_label = QLabel("Brightness") | |
self.brightness_slider = QSlider(Qt.Orientation.Horizontal) | |
self.brightness_slider.setMinimum(1) | |
self.brightness_slider.setMaximum(99) | |
self.brightness_slider.setValue(50) # Default middle | |
self.brightness_slider.valueChanged.connect(self.update_brightness) | |
layout.addWidget(self.brightness_label) | |
layout.addWidget(self.brightness_slider) | |
# Off Button | |
self.off_button = QPushButton("Turn Off") | |
self.off_button.clicked.connect(turn_off) | |
layout.addWidget(self.off_button) | |
# Set layout | |
self.setLayout(layout) | |
def pick_color(self, color: QColor): | |
"""Updates the color instantly from the embedded color picker.""" | |
r, g, b = color.red(), color.green(), color.blue() | |
set_color(r, g, b) | |
def update_white(self): | |
"""Updates white level based on slider value.""" | |
level = self.white_slider.value() | |
set_white(level) | |
def update_brightness(self): | |
"""Updates brightness based on slider value.""" | |
level = self.brightness_slider.value() | |
set_brightness(level) | |
# Run the PyQt6 UI | |
if __name__ == "__main__": | |
connect_to_device() # Establish connection once | |
app = QApplication(sys.argv) | |
window = LightControlUI() | |
window.show() | |
sys.exit(app.exec()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment