loader image

Reply To: Logitech Litra Glow (Linux Drivers)

What makes us different from other similar websites? Forums Tech Logitech Litra Glow (Linux Drivers) Reply To: Logitech Litra Glow (Linux Drivers)

#8480
thumbtak
Moderator

Guide: How to Setup and Build Litra CLI (lcli) on Xubuntu / Ubuntu

If you are using a Logitech Litra Glow or Beam on an Ubuntu-based system and want to use community tools like lcli (or build a custom Python Tkinter GUI wrapper for it), you will need to compile the binary from source and set the correct hardware permissions.

Here is the complete sequence of terminal commands to get everything working from scratch.

1. Install System Dependencies
First, install the Python Tkinter library (if you plan to run a GUI), the Go compiler, and the necessary C-development headers for USB device communication (libudev):

$ sudo apt update
$ sudo apt install python3-tk golang-go libudev-dev

2. Download and Extract the Source Code
Navigate to your Downloads folder, download the source code zip from the repository, and unzip it:

$ cd ~/Downloads
$ wget https://github.com/kharyam/go-litra-driver/archive/refs/heads/main.zip
$ unzip main.zip

3. Compile the Binary
Navigate into the lcli source directory and compile the program using Go. (Note: If the build completes successfully, the terminal will return to a blank line without printing any text):

$ cd go-litra-driver-main/lcli
$ go build

4. Install lcli Globally
Move the newly compiled executable file into your system’s global binary folder so it can be run from anywhere, then grant it execution permissions:

$ sudo cp lcli /usr/local/bin/lcli
$ sudo chmod +x /usr/local/bin/lcli

5. Configure USB Permissions (udev Rule)
By default, Linux restricts raw access to USB devices. Create a udev rule so your user account can communicate with the Litra device without needing sudo:

$ sudo tee /etc/udev/rules.d/82-litra-glow.rules <<< 'SUBSYSTEM=="usb", ATTR{idVendor}=="046d", ATTR{idProduct}=="c900", MODE="0666"'

(Note: If you are using a Litra Beam instead of a Glow, change c900 to c901)

6. Apply Rules and Test
Reload the system rules to apply the changes, and unplug/replug your Litra light’s USB cable. Then, test that the command works directly from the terminal:

$ sudo udevadm control --reload-rules && sudo udevadm trigger
$ lcli on

7. Run Your Python GUI (Refer to 8 for the program code)
Go back to your main directory and launch your Python controller script:

import tkinter as tk
from tkinter import ttk, simpledialog
import subprocess

# --- Configuration Constants ---
DEFAULT_CLI_PATH = '/usr/local/bin/lcli'
MIN_BRIGHTNESS = 1
MAX_BRIGHTNESS = 100
MIN_TEMP = 2700
MAX_TEMP = 6500

# High-Contrast Pastel Palette (No Greens/Teals)
BG_PASTEL_MAIN = "#FFF0F5" # Lavender Blush / Soft Pink Tint
BG_PASTEL_CARD = "#FFFFFF" # Crisp White for clean contrast
TEXT_MAIN = "#3B203E" # Deepened Dark Plum for maximum readability
TEXT_MUTED = "#7D5C86" # Darker Mauve for headers to ensure visibility
ACCENT_PINK = "#FF94B8" # Slightly richer pastel pink for status text/active buttons
SLIDER_TROUGH = "#FCD7E4" # Highly visible pink tint for slider tracks

class LitraGlowStudio(tk.Tk):
def __init__(self):
super().__init__()
self.title("Litra Glow Studio")
self.geometry("340x290") # Sized slightly up to prevent text cramping
self.resizable(False, False)
self.configure(bg=BG_PASTEL_MAIN)

self.cli_path = DEFAULT_CLI_PATH
self.is_power_on = tk.BooleanVar(value=True)

# Debounce tracking to keep live slider streaming fluid
self.pending_update = None

self.setup_styles()
self.create_widgets()
self.update_hardware(trigger_execution=False)

def setup_styles(self):
s = ttk.Style()
try:
s.theme_use('clam')
except:
pass

s.configure('.', background=BG_PASTEL_MAIN, foreground=TEXT_MAIN)
s.configure('TFrame', background=BG_PASTEL_MAIN)
s.configure('Card.TFrame', background=BG_PASTEL_CARD)

# High-contrast text on the buttons
s.configure('PowerOn.TButton', background=ACCENT_PINK, foreground="#FFFFFF", font=('Arial', 10, 'bold'), borderwidth=0)
s.map('PowerOn.TButton', background=[('active', '#FF709F')])

s.configure('PowerOff.TButton', background=BG_PASTEL_CARD, foreground=TEXT_MUTED, font=('Arial', 10, 'bold'), borderwidth=1, bordercolor=SLIDER_TROUGH)
s.map('PowerOff.TButton', background=[('active', BG_PASTEL_MAIN)])

s.configure('Settings.TButton', background=BG_PASTEL_MAIN, foreground=TEXT_MUTED, font=('Arial', 11), borderwidth=0)
s.map('Settings.TButton', background=[('active', BG_PASTEL_CARD)], foreground=[('active', TEXT_MAIN)])

def create_widgets(self):
# Header Area
header_frame = ttk.Frame(self, padding=(15, 10, 15, 5))
header_frame.pack(fill='x')

title_label = tk.Label(header_frame, text="Litra Glow Studio", font=('Arial', 13, 'bold'), bg=BG_PASTEL_MAIN, fg=TEXT_MAIN)
title_label.pack(side='left')

settings_btn = ttk.Button(header_frame, text="⚙", width=3, style='Settings.TButton', command=self.open_settings)
settings_btn.pack(side='right')

# Main Control Card
card_frame = ttk.Frame(self, padding="15", style='Card.TFrame')
card_frame.pack(fill='both', expand=True, padx=15, pady=(5, 15))

# Row 1: Power Status
power_frame = tk.Frame(card_frame, bg=BG_PASTEL_CARD)
power_frame.pack(fill='x', pady=(0, 10))

self.status_label = tk.Label(power_frame, text="Light is On ✨", font=('Arial', 11, 'bold'), bg=BG_PASTEL_CARD, fg=ACCENT_PINK)
self.status_label.pack(side='left', anchor='w')

self.power_btn = ttk.Button(power_frame, text="ON", width=8, command=self.toggle_power)
self.power_btn.pack(side='right')

# Shared slider configurations (Darkened values for easier reading)
slider_opts = {
"orient": tk.HORIZONTAL, "bg": BG_PASTEL_CARD, "fg": TEXT_MAIN, "troughcolor": SLIDER_TROUGH,
"highlightthickness": 0, "bd": 0, "activebackground": ACCENT_PINK, "font": ('Arial', 9, 'bold')
}

# Row 2: Live Brightness Slider
tk.Label(card_frame, text="BRIGHTNESS", font=('Arial', 9, 'bold'), bg=BG_PASTEL_CARD, fg=TEXT_MUTED).pack(anchor='w', pady=(5, 0))
self.bright_scale = tk.Scale(card_frame, from_=MIN_BRIGHTNESS, to=MAX_BRIGHTNESS, resolution=1,
command=self.on_slider_move, **slider_opts)
self.bright_scale.set(50)
self.bright_scale.pack(fill='x', pady=(0, 8))

# Row 3: Live Color Temperature Slider
tk.Label(card_frame, text="COLOR TEMPERATURE (K)", font=('Arial', 9, 'bold'), bg=BG_PASTEL_CARD, fg=TEXT_MUTED).pack(anchor='w', pady=(5, 0))
self.temp_scale = tk.Scale(card_frame, from_=MIN_TEMP, to=MAX_TEMP, resolution=50,
command=self.on_slider_move, **slider_opts)
self.temp_scale.set(4600)
self.temp_scale.pack(fill='x', pady=(0, 15))

# Row 4: Professional Bottom Branding Line
branding_label = tk.Label(card_frame, text="created by thumbtak on TAKsShack.com",
font=('Arial', 8, 'italic'), bg=BG_PASTEL_CARD, fg=TEXT_MUTED)
branding_label.pack(side='bottom', pady=(5, 0))

self.update_ui_state()

def toggle_power(self):
self.is_power_on.set(not self.is_power_on.get())
self.update_ui_state()
self.update_hardware(trigger_execution=True)

def update_ui_state(self):
if self.is_power_on.get():
self.power_btn.config(text="ON", style='PowerOn.TButton')
self.status_label.config(text="Light is On ✨", fg=ACCENT_PINK)
self.bright_scale.config(state='normal', fg=TEXT_MAIN)
self.temp_scale.config(state='normal', fg=TEXT_MAIN)
else:
self.power_btn.config(text="OFF", style='PowerOff.TButton')
self.status_label.config(text="Sleeping 💤", fg=TEXT_MUTED)
self.bright_scale.config(state='disabled', fg=SLIDER_TROUGH)
self.temp_scale.config(state='disabled', fg=SLIDER_TROUGH)

def open_settings(self):
new_path = simpledialog.askstring("CLI Settings", "Modify global 'lcli' execution path:", initialvalue=self.cli_path, parent=self)
if new_path:
self.cli_path = new_path.strip()

def on_slider_move(self, value):
if self.pending_update:
self.after_cancel(self.pending_update)
self.pending_update = self.after(50, lambda: self.update_hardware(trigger_execution=True))

def update_hardware(self, trigger_execution=False):
if not trigger_execution:
return

brightness = self.bright_scale.get()
temperature = self.temp_scale.get()

if self.is_power_on.get():
command = f"{self.cli_path} on && {self.cli_path} bright {brightness} && {self.cli_path} temp {temperature}"
else:
command = f"{self.cli_path} off"

try:
subprocess.Popen(command, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
except Exception:
pass

if __name__ == "__main__":
app = LitraGlowStudio()
app.mainloop()
  • This reply was modified 1 month ago by thumbtak. Reason: Updated Program
TAKs Shack