Simple RGB LED controller for Mac OS X
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

caselights 9.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. #!/usr/bin/env python3
  2. # CaseLights Linux Qt System Tray client
  3. # depends on:
  4. # - python-pyqt5
  5. # - python-pyserial
  6. import subprocess
  7. import sys
  8. import os.path
  9. import threading
  10. import time
  11. import colorsys
  12. import serial, serial.tools, serial.tools.list_ports
  13. from PyQt5 import QtWidgets, QtGui, QtCore
  14. from PyQt5.QtWidgets import QSystemTrayIcon, QAction, QMenu
  15. from PyQt5.QtGui import QIcon, QPixmap, QCursor
  16. from PyQt5.QtCore import QCoreApplication, QSettings
  17. class CaseLights():
  18. name = "CaseLights"
  19. vendor = "xythobuz"
  20. version = "0.2"
  21. iconPath = "/usr/share/pixmaps/"
  22. iconName = "caselights_icon.png"
  23. staticColors = [
  24. [ "Off", "0", "0", "0", None ],
  25. [ "Red", "255", "0", "0", None ],
  26. [ "Green", "0", "255", "0", None ],
  27. [ "Blue", "0", "0", "255", None ],
  28. [ "White", "255", "255", "255", None ],
  29. ]
  30. slowFadeUpdateFreq = 5
  31. fastFadeUpdateFreq = 20
  32. cpuUsageUpdateFreq = 2
  33. fadeSaturation = 1.0
  34. fadeValue = 1.0
  35. fadeHueCounter = 0
  36. usedPort = None
  37. serial = None
  38. animation = None
  39. animationRunning = False
  40. menu = None
  41. portMenu = None
  42. portActions = None
  43. refreshAction = None
  44. quitAction = None
  45. def __init__(self):
  46. app = QtWidgets.QApplication(sys.argv)
  47. QCoreApplication.setApplicationName(self.name)
  48. if not QSystemTrayIcon.isSystemTrayAvailable():
  49. print("System Tray is not available on this platform!")
  50. sys.exit(0)
  51. self.readSettings()
  52. if self.usedPort is not None:
  53. self.connect()
  54. self.menu = QMenu()
  55. colorMenu = QMenu("&Colors")
  56. for color in self.staticColors:
  57. color[4] = QAction(color[0])
  58. colorMenu.addAction(color[4])
  59. colorMenu.triggered.connect(self.setStaticColor)
  60. self.menu.addMenu(colorMenu)
  61. animMenu = QMenu("&Animations")
  62. noFadeAction = QAction("Off")
  63. noFadeAction.triggered.connect(self.animOff)
  64. animMenu.addAction(noFadeAction)
  65. slowFadeAction = QAction("Slow Fade")
  66. slowFadeAction.triggered.connect(self.slowFadeOn)
  67. animMenu.addAction(slowFadeAction)
  68. fastFadeAction = QAction("Fast Fade")
  69. fastFadeAction.triggered.connect(self.fastFadeOn)
  70. animMenu.addAction(fastFadeAction)
  71. self.menu.addMenu(animMenu)
  72. visualMenu = QMenu("&Visualizations")
  73. noVisualAction = QAction("Off")
  74. noVisualAction.triggered.connect(self.animOff)
  75. visualMenu.addAction(noVisualAction)
  76. cpuUsageAction = QAction("CPU Usage")
  77. cpuUsageAction.triggered.connect(self.cpuUsageOn)
  78. visualMenu.addAction(cpuUsageAction)
  79. self.menu.addMenu(visualMenu)
  80. lightMenu = QMenu("&UV-Light")
  81. lightOnAction = QAction("O&n")
  82. lightOnAction.triggered.connect(self.lightsOn)
  83. lightMenu.addAction(lightOnAction)
  84. lightOffAction = QAction("O&ff")
  85. lightOffAction.triggered.connect(self.lightsOff)
  86. lightMenu.addAction(lightOffAction)
  87. self.menu.addMenu(lightMenu)
  88. self.refreshSerialPorts()
  89. self.quitAction = QAction("&Quit")
  90. self.quitAction.triggered.connect(self.exit)
  91. self.menu.addAction(self.quitAction)
  92. iconPathName = ""
  93. if os.path.isfile(self.iconName):
  94. iconPathName = self.iconName
  95. elif os.path.isfile(self.iconPath + self.iconName):
  96. iconPathName = self.iconPath + self.iconName
  97. else:
  98. print("no icon found")
  99. icon = QIcon()
  100. if iconPathName != "":
  101. pic = QPixmap(32, 32)
  102. pic.load(iconPathName)
  103. icon = QIcon(pic)
  104. trayIcon = QSystemTrayIcon(icon)
  105. trayIcon.setToolTip(self.name + " " + self.version)
  106. trayIcon.setContextMenu(self.menu)
  107. trayIcon.activated.connect(self.showHide)
  108. trayIcon.setVisible(True)
  109. sys.exit(app.exec_())
  110. def showHide(self, activationReason):
  111. if activationReason == QSystemTrayIcon.Trigger:
  112. self.menu.popup(QCursor.pos())
  113. def exit(self):
  114. if self.serial is not None:
  115. if self.serial.is_open:
  116. print("stopping animations")
  117. self.animOff()
  118. print("turning off lights")
  119. self.serial.write(b'RGB 0 0 0\n')
  120. self.serial.write(b'UV 0\n')
  121. print("closing connection")
  122. self.serial.close()
  123. QCoreApplication.quit()
  124. def readSettings(self):
  125. settings = QSettings(self.vendor, self.name)
  126. self.usedPort = settings.value("serial_port")
  127. if self.usedPort is not None:
  128. print("serial port stored: " + self.usedPort)
  129. else:
  130. print("no serial port stored")
  131. def writeSettings(self):
  132. settings = QSettings(self.vendor, self.name)
  133. settings.setValue("serial_port", self.usedPort)
  134. if self.usedPort is not None:
  135. print("storing serial port: " + self.usedPort)
  136. else:
  137. print("not storing any serial port")
  138. del settings
  139. def refreshSerialPorts(self):
  140. self.portMenu = QMenu("Port")
  141. ports = serial.tools.list_ports.comports()
  142. self.portActions = []
  143. for port in ports:
  144. action = QAction(port.device)
  145. self.portActions.append(action)
  146. self.portMenu.addAction(action)
  147. self.portMenu.triggered.connect(self.selectSerialPort)
  148. if self.refreshAction == None:
  149. self.refreshAction = QAction("&Refresh")
  150. self.refreshAction.triggered.connect(self.refreshSerialPorts)
  151. self.portMenu.addAction(self.refreshAction)
  152. self.menu.insertMenu(self.quitAction, self.portMenu)
  153. def selectSerialPort(self, action):
  154. self.usedPort = action.text()
  155. self.writeSettings()
  156. if self.connect():
  157. self.portMenu.setActiveAction(action)
  158. def connect(self):
  159. if self.usedPort is None:
  160. print("not connecting to any serial port")
  161. return False
  162. if self.serial is not None:
  163. print("closing previous port")
  164. self.serial.close()
  165. self.serial = serial.Serial()
  166. self.serial.port = self.usedPort
  167. self.serial.baudrate = 115200
  168. self.serial.open()
  169. if self.serial.is_open:
  170. print("connected to: " + self.usedPort)
  171. else:
  172. print("error connecting to: " + self.usedPort)
  173. return self.serial.is_open
  174. def printRGBStrings(self, rs, gs, bs):
  175. if self.serial.is_open:
  176. r = str.encode(rs)
  177. g = str.encode(gs)
  178. b = str.encode(bs)
  179. rgb = b'RGB ' + r + b' ' + g + b' ' + b + b'\n'
  180. self.serial.write(rgb)
  181. else:
  182. print("not connected")
  183. def setStaticColor(self, action):
  184. self.animOff()
  185. for color in self.staticColors:
  186. if color[4] is action:
  187. self.printRGBStrings(color[1], color[2], color[3])
  188. return True
  189. print("color not found")
  190. return False
  191. def hsvToRgb(self, h, s, v):
  192. (r, g, b) = colorsys.hsv_to_rgb(h, s, v)
  193. return (round(r * 255), round(g * 255), round(b * 255))
  194. def getCurrentCpuUsage(self):
  195. # https://stackoverflow.com/a/9229692
  196. # "top -bn1 | grep "Cpu(s)" | sed "s/.*, *\([0-9.]*\)%* id.*/\1/" | awk '{print 100 - $1}'"
  197. # https://stackoverflow.com/a/4760517
  198. cmd = ["top -bn1 | grep \"Cpu(s)\" | sed \"s/.*, *\\([0-9.]*\\)%* id.*/\\1/\" | awk '{print 100 - $1}'"]
  199. result = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE)
  200. num = result.stdout.decode('utf-8')
  201. return float(num)
  202. def cpuUsageRunner(self):
  203. while self.animationRunning is True:
  204. cpu = self.getCurrentCpuUsage()
  205. color = cpu / 100.0 * 120.0
  206. (r, g, b) = self.hsvToRgb((120.0 - color) / 360.0, self.fadeSaturation, self.fadeValue)
  207. self.printRGBStrings(str(r), str(g), str(b))
  208. time.sleep(1.0 / self.cpuUsageUpdateFreq)
  209. def cpuUsageOn(self):
  210. self.animOff()
  211. self.animationRunning = True
  212. self.animation = threading.Thread(target=self.cpuUsageRunner)
  213. self.animation.start()
  214. def fadeRunner(self, freq):
  215. while self.animationRunning is True:
  216. self.fadeHueCounter += 1
  217. if self.fadeHueCounter >= 360:
  218. self.fadeHueCounter = 0
  219. (r, g, b) = self.hsvToRgb(self.fadeHueCounter / 360.0, self.fadeSaturation, self.fadeValue)
  220. self.printRGBStrings(str(r), str(g), str(b))
  221. time.sleep(1.0 / freq)
  222. def slowFadeOn(self):
  223. self.animOff()
  224. self.animationRunning = True
  225. self.animation = threading.Thread(target=self.fadeRunner, args=[self.slowFadeUpdateFreq])
  226. self.animation.start()
  227. def fastFadeOn(self):
  228. self.animOff()
  229. self.animationRunning = True
  230. self.animation = threading.Thread(target=self.fadeRunner, args=[self.fastFadeUpdateFreq])
  231. self.animation.start()
  232. def animOff(self):
  233. self.animationRunning = False
  234. if self.animation != None:
  235. self.animation.join()
  236. self.animation = None
  237. self.printRGBStrings("0", "0", "0")
  238. time.sleep(0.1)
  239. def lightsOn(self):
  240. if self.serial.is_open:
  241. self.serial.write(b'UV 1\n')
  242. else:
  243. print("not connected")
  244. def lightsOff(self):
  245. if self.serial.is_open:
  246. self.serial.write(b'UV 0\n')
  247. else:
  248. print("not connected")
  249. if __name__ == "__main__":
  250. tray = CaseLights()