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.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  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
  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.setVisible(True)
  108. sys.exit(app.exec_())
  109. def exit(self):
  110. if self.serial is not None:
  111. if self.serial.is_open:
  112. print("stopping animations")
  113. self.animOff()
  114. print("turning off lights")
  115. self.serial.write(b'RGB 0 0 0\n')
  116. self.serial.write(b'UV 0\n')
  117. print("closing connection")
  118. self.serial.close()
  119. QCoreApplication.quit()
  120. def readSettings(self):
  121. settings = QSettings(self.vendor, self.name)
  122. self.usedPort = settings.value("serial_port")
  123. if self.usedPort is not None:
  124. print("serial port stored: " + self.usedPort)
  125. else:
  126. print("no serial port stored")
  127. def writeSettings(self):
  128. settings = QSettings(self.vendor, self.name)
  129. settings.setValue("serial_port", self.usedPort)
  130. if self.usedPort is not None:
  131. print("storing serial port: " + self.usedPort)
  132. else:
  133. print("not storing any serial port")
  134. del settings
  135. def refreshSerialPorts(self):
  136. self.portMenu = QMenu("Port")
  137. ports = serial.tools.list_ports.comports()
  138. self.portActions = []
  139. for port in ports:
  140. action = QAction(port.device)
  141. self.portActions.append(action)
  142. self.portMenu.addAction(action)
  143. self.portMenu.triggered.connect(self.selectSerialPort)
  144. if self.refreshAction == None:
  145. self.refreshAction = QAction("&Refresh")
  146. self.refreshAction.triggered.connect(self.refreshSerialPorts)
  147. self.portMenu.addAction(self.refreshAction)
  148. self.menu.insertMenu(self.quitAction, self.portMenu)
  149. def selectSerialPort(self, action):
  150. self.usedPort = action.text()
  151. self.writeSettings()
  152. if self.connect():
  153. self.portMenu.setActiveAction(action)
  154. def connect(self):
  155. if self.usedPort is None:
  156. print("not connecting to any serial port")
  157. return False
  158. if self.serial is not None:
  159. print("closing previous port")
  160. self.serial.close()
  161. self.serial = serial.Serial()
  162. self.serial.port = self.usedPort
  163. self.serial.baudrate = 115200
  164. self.serial.open()
  165. if self.serial.is_open:
  166. print("connected to: " + self.usedPort)
  167. else:
  168. print("error connecting to: " + self.usedPort)
  169. return self.serial.is_open
  170. def printRGBStrings(self, rs, gs, bs):
  171. if self.serial.is_open:
  172. r = str.encode(rs)
  173. g = str.encode(gs)
  174. b = str.encode(bs)
  175. rgb = b'RGB ' + r + b' ' + g + b' ' + b + b'\n'
  176. self.serial.write(rgb)
  177. else:
  178. print("not connected")
  179. def setStaticColor(self, action):
  180. self.animOff()
  181. for color in self.staticColors:
  182. if color[4] is action:
  183. self.printRGBStrings(color[1], color[2], color[3])
  184. return True
  185. print("color not found")
  186. return False
  187. def hsvToRgb(self, h, s, v):
  188. (r, g, b) = colorsys.hsv_to_rgb(h, s, v)
  189. return (round(r * 255), round(g * 255), round(b * 255))
  190. def getCurrentCpuUsage(self):
  191. # https://stackoverflow.com/a/9229692
  192. # "top -bn1 | grep "Cpu(s)" | sed "s/.*, *\([0-9.]*\)%* id.*/\1/" | awk '{print 100 - $1}'"
  193. # https://stackoverflow.com/a/4760517
  194. cmd = ["top -bn1 | grep \"Cpu(s)\" | sed \"s/.*, *\\([0-9.]*\\)%* id.*/\\1/\" | awk '{print 100 - $1}'"]
  195. result = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE)
  196. num = result.stdout.decode('utf-8')
  197. return float(num)
  198. def cpuUsageRunner(self):
  199. while self.animationRunning is True:
  200. cpu = self.getCurrentCpuUsage()
  201. color = cpu / 100.0 * 120.0
  202. (r, g, b) = self.hsvToRgb((120.0 - color) / 360.0, self.fadeSaturation, self.fadeValue)
  203. self.printRGBStrings(str(r), str(g), str(b))
  204. time.sleep(1.0 / self.cpuUsageUpdateFreq)
  205. def cpuUsageOn(self):
  206. self.animOff()
  207. self.animationRunning = True
  208. self.animation = threading.Thread(target=self.cpuUsageRunner)
  209. self.animation.start()
  210. def fadeRunner(self, freq):
  211. while self.animationRunning is True:
  212. self.fadeHueCounter += 1
  213. if self.fadeHueCounter >= 360:
  214. self.fadeHueCounter = 0
  215. (r, g, b) = self.hsvToRgb(self.fadeHueCounter / 360.0, self.fadeSaturation, self.fadeValue)
  216. self.printRGBStrings(str(r), str(g), str(b))
  217. time.sleep(1.0 / freq)
  218. def slowFadeOn(self):
  219. self.animOff()
  220. self.animationRunning = True
  221. self.animation = threading.Thread(target=self.fadeRunner, args=[self.slowFadeUpdateFreq])
  222. self.animation.start()
  223. def fastFadeOn(self):
  224. self.animOff()
  225. self.animationRunning = True
  226. self.animation = threading.Thread(target=self.fadeRunner, args=[self.fastFadeUpdateFreq])
  227. self.animation.start()
  228. def animOff(self):
  229. self.animationRunning = False
  230. if self.animation != None:
  231. self.animation.join()
  232. self.animation = None
  233. self.printRGBStrings("0", "0", "0")
  234. time.sleep(0.1)
  235. def lightsOn(self):
  236. if self.serial.is_open:
  237. self.serial.write(b'UV 1\n')
  238. else:
  239. print("not connected")
  240. def lightsOff(self):
  241. if self.serial.is_open:
  242. self.serial.write(b'UV 0\n')
  243. else:
  244. print("not connected")
  245. if __name__ == "__main__":
  246. tray = CaseLights()