#!/usr/bin/env python3 # CaseLights Linux Qt System Tray client # depends on: # - python-pyqt5 # - python-pyserial import subprocess import sys import os.path import threading import time import colorsys import serial, serial.tools, serial.tools.list_ports from PyQt5 import QtWidgets, QtGui, QtCore from PyQt5.QtWidgets import QSystemTrayIcon, QAction, QMenu from PyQt5.QtGui import QIcon, QPixmap from PyQt5.QtCore import QCoreApplication, QSettings class CaseLights(): name = "CaseLights" vendor = "xythobuz" version = "0.2" iconPath = "/usr/share/pixmaps/" iconName = "caselights_icon.png" staticColors = [ [ "Off", "0", "0", "0", None ], [ "Red", "255", "0", "0", None ], [ "Green", "0", "255", "0", None ], [ "Blue", "0", "0", "255", None ], [ "White", "255", "255", "255", None ], ] slowFadeUpdateFreq = 5 fastFadeUpdateFreq = 20 cpuUsageUpdateFreq = 2 fadeSaturation = 1.0 fadeValue = 1.0 fadeHueCounter = 0 usedPort = None serial = None animation = None animationRunning = False menu = None portMenu = None portActions = None refreshAction = None quitAction = None def __init__(self): app = QtWidgets.QApplication(sys.argv) QCoreApplication.setApplicationName(self.name) if not QSystemTrayIcon.isSystemTrayAvailable(): print("System Tray is not available on this platform!") sys.exit(0) self.readSettings() if self.usedPort is not None: self.connect() self.menu = QMenu() colorMenu = QMenu("&Colors") for color in self.staticColors: color[4] = QAction(color[0]) colorMenu.addAction(color[4]) colorMenu.triggered.connect(self.setStaticColor) self.menu.addMenu(colorMenu) animMenu = QMenu("&Animations") noFadeAction = QAction("Off") noFadeAction.triggered.connect(self.animOff) animMenu.addAction(noFadeAction) slowFadeAction = QAction("Slow Fade") slowFadeAction.triggered.connect(self.slowFadeOn) animMenu.addAction(slowFadeAction) fastFadeAction = QAction("Fast Fade") fastFadeAction.triggered.connect(self.fastFadeOn) animMenu.addAction(fastFadeAction) self.menu.addMenu(animMenu) visualMenu = QMenu("&Visualizations") noVisualAction = QAction("Off") noVisualAction.triggered.connect(self.animOff) visualMenu.addAction(noVisualAction) cpuUsageAction = QAction("CPU Usage") cpuUsageAction.triggered.connect(self.cpuUsageOn) visualMenu.addAction(cpuUsageAction) self.menu.addMenu(visualMenu) lightMenu = QMenu("&UV-Light") lightOnAction = QAction("O&n") lightOnAction.triggered.connect(self.lightsOn) lightMenu.addAction(lightOnAction) lightOffAction = QAction("O&ff") lightOffAction.triggered.connect(self.lightsOff) lightMenu.addAction(lightOffAction) self.menu.addMenu(lightMenu) self.refreshSerialPorts() self.quitAction = QAction("&Quit") self.quitAction.triggered.connect(self.exit) self.menu.addAction(self.quitAction) iconPathName = "" if os.path.isfile(self.iconName): iconPathName = self.iconName elif os.path.isfile(self.iconPath + self.iconName): iconPathName = self.iconPath + self.iconName else: print("no icon found") icon = QIcon() if iconPathName != "": pic = QPixmap(32, 32) pic.load(iconPathName) icon = QIcon(pic) trayIcon = QSystemTrayIcon(icon) trayIcon.setToolTip(self.name + " " + self.version) trayIcon.setContextMenu(self.menu) trayIcon.setVisible(True) sys.exit(app.exec_()) def exit(self): if self.serial is not None: if self.serial.is_open: print("stopping animations") self.animOff() print("turning off lights") self.serial.write(b'RGB 0 0 0\n') self.serial.write(b'UV 0\n') print("closing connection") self.serial.close() QCoreApplication.quit() def readSettings(self): settings = QSettings(self.vendor, self.name) self.usedPort = settings.value("serial_port") if self.usedPort is not None: print("serial port stored: " + self.usedPort) else: print("no serial port stored") def writeSettings(self): settings = QSettings(self.vendor, self.name) settings.setValue("serial_port", self.usedPort) if self.usedPort is not None: print("storing serial port: " + self.usedPort) else: print("not storing any serial port") del settings def refreshSerialPorts(self): self.portMenu = QMenu("Port") ports = serial.tools.list_ports.comports() self.portActions = [] for port in ports: action = QAction(port.device) self.portActions.append(action) self.portMenu.addAction(action) self.portMenu.triggered.connect(self.selectSerialPort) if self.refreshAction == None: self.refreshAction = QAction("&Refresh") self.refreshAction.triggered.connect(self.refreshSerialPorts) self.portMenu.addAction(self.refreshAction) self.menu.insertMenu(self.quitAction, self.portMenu) def selectSerialPort(self, action): self.usedPort = action.text() self.writeSettings() if self.connect(): self.portMenu.setActiveAction(action) def connect(self): if self.usedPort is None: print("not connecting to any serial port") return False if self.serial is not None: print("closing previous port") self.serial.close() self.serial = serial.Serial() self.serial.port = self.usedPort self.serial.baudrate = 115200 self.serial.open() if self.serial.is_open: print("connected to: " + self.usedPort) else: print("error connecting to: " + self.usedPort) return self.serial.is_open def printRGBStrings(self, rs, gs, bs): if self.serial.is_open: r = str.encode(rs) g = str.encode(gs) b = str.encode(bs) rgb = b'RGB ' + r + b' ' + g + b' ' + b + b'\n' self.serial.write(rgb) else: print("not connected") def setStaticColor(self, action): self.animOff() for color in self.staticColors: if color[4] is action: self.printRGBStrings(color[1], color[2], color[3]) return True print("color not found") return False def hsvToRgb(self, h, s, v): (r, g, b) = colorsys.hsv_to_rgb(h, s, v) return (round(r * 255), round(g * 255), round(b * 255)) def getCurrentCpuUsage(self): # https://stackoverflow.com/a/9229692 # "top -bn1 | grep "Cpu(s)" | sed "s/.*, *\([0-9.]*\)%* id.*/\1/" | awk '{print 100 - $1}'" # https://stackoverflow.com/a/4760517 cmd = ["top -bn1 | grep \"Cpu(s)\" | sed \"s/.*, *\\([0-9.]*\\)%* id.*/\\1/\" | awk '{print 100 - $1}'"] result = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE) num = result.stdout.decode('utf-8') return float(num) def cpuUsageRunner(self): while self.animationRunning is True: cpu = self.getCurrentCpuUsage() color = cpu / 100.0 * 120.0 (r, g, b) = self.hsvToRgb((120.0 - color) / 360.0, self.fadeSaturation, self.fadeValue) self.printRGBStrings(str(r), str(g), str(b)) time.sleep(1.0 / self.cpuUsageUpdateFreq) def cpuUsageOn(self): self.animOff() self.animationRunning = True self.animation = threading.Thread(target=self.cpuUsageRunner) self.animation.start() def fadeRunner(self, freq): while self.animationRunning is True: self.fadeHueCounter += 1 if self.fadeHueCounter >= 360: self.fadeHueCounter = 0 (r, g, b) = self.hsvToRgb(self.fadeHueCounter / 360.0, self.fadeSaturation, self.fadeValue) self.printRGBStrings(str(r), str(g), str(b)) time.sleep(1.0 / freq) def slowFadeOn(self): self.animOff() self.animationRunning = True self.animation = threading.Thread(target=self.fadeRunner, args=[self.slowFadeUpdateFreq]) self.animation.start() def fastFadeOn(self): self.animOff() self.animationRunning = True self.animation = threading.Thread(target=self.fadeRunner, args=[self.fastFadeUpdateFreq]) self.animation.start() def animOff(self): self.animationRunning = False if self.animation != None: self.animation.join() self.animation = None self.printRGBStrings("0", "0", "0") time.sleep(0.1) def lightsOn(self): if self.serial.is_open: self.serial.write(b'UV 1\n') else: print("not connected") def lightsOff(self): if self.serial.is_open: self.serial.write(b'UV 0\n') else: print("not connected") if __name__ == "__main__": tray = CaseLights()