IdeasXWSCBackend.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  1. '''
  2. Title: IdeasXDatabaseManager Class
  3. Author: Tyler Berezowsky
  4. Description:
  5. This class requires the following functionality:
  6. 1) Connect to the IdeasX system (MQTT Client)
  7. - Connect using the devices MAC Address as the Client ID
  8. - Autoreconnect if the device failts
  9. - The abililty to start a broker in a seperate thread if no broker is available
  10. - The ability to store settings in a SQLite File or setting .txt file.
  11. 2) The ability to induce a system wide keypress in the following systems:
  12. - Windows
  13. - Mac
  14. - Linux
  15. 3) Create a table in memory of the IdeasX devices currently in the system
  16. 4) Parse IdeasX messages types given nothing more than a protofile
  17. 5) Subscribe to IdeasX devices
  18. 6) Invoke keystrokes if proper messages in a command is sent.
  19. '''
  20. import sys
  21. import time
  22. import collections
  23. from ParsingTools import ParsingTools
  24. try:
  25. import paho.mqtt.client as mqtt
  26. import paho.mqtt.publish as mqtt_pub
  27. except ImportError:
  28. # This part is only required to run the example from within the examples
  29. # directory when the module itself is not installed.
  30. #
  31. # If you have the module installed, just use "import paho.mqtt.client"
  32. import os
  33. import inspect
  34. cmd_subfolder = os.path.realpath(os.path.abspath(os.path.join(os.path.split(inspect.getfile( inspect.currentframe() ))[0],"../src")))
  35. if cmd_subfolder not in sys.path:
  36. sys.path.insert(0, cmd_subfolder)
  37. import paho.mqtt.client as mqtt
  38. try:
  39. from protocolbuffers import IdeasXMessages_pb2 as IdeasXMessages
  40. except ImportError:
  41. print("The python classes for IdeasX are missing. Try running the Makefile in" +
  42. "ideasX-messages.")
  43. from PyQt5.QtCore import QObject, pyqtSignal, QSettings
  44. class IdeasXWSCNetworkThread(QObject):
  45. # define Qt signals (I don't understand why this is here)
  46. encoderUpdate = pyqtSignal([dict], name='encoderUpdate')
  47. networkStatus = pyqtSignal([str], name='networkStatus')
  48. networkUpdate = pyqtSignal([str], name='networkUpdate')
  49. settingsError = pyqtSignal([str], name='settingsError')
  50. def __init__(self, settingFile=None, clientID = None, debug=True, mqttdebug=True):
  51. super(IdeasXWSCNetworkThread, self).__init__()
  52. # Private Class Flags and Variables
  53. self.__clientID = clientID
  54. self.__settingFile = settingFile
  55. self.__debug = debug
  56. self.__mqttDebug = mqttdebug
  57. self.__errorIndex = 0
  58. self.__refreshCb = None
  59. self.__org = 'IdeasX'
  60. self.__app = 'Workstation-Client'
  61. # MQTT Topics
  62. self.__DEVICETYPE = ["/encoder/+"]
  63. self.__COMMANDTOPIC = "/command"
  64. self.__DATATOPIC = "/data"
  65. self.__HEALTHTOPIC = "/health"
  66. # Data Structure for Encoders / Actuators
  67. self.encoders = {}
  68. self.subscribedEncoders = []
  69. # IdeasX Parsers
  70. self._healthParser = IdeasXMessages.HealthMessage()
  71. self._dataParser = IdeasXMessages.DataMessage()
  72. self._commandParser = IdeasXMessages.CommandMessage()
  73. self._parserTools = ParsingTools()
  74. self.keyEmulator = IdeasXKeyEmulator()
  75. # MQTT Client Object
  76. self._mqttc = mqtt.Client(self.__clientID, clean_session=True, userdata=None, protocol='MQTTv311')
  77. # Setup Callback Functions for each device type
  78. for device in self.__DEVICETYPE:
  79. self._mqttc.message_callback_add(device+self.__HEALTHTOPIC, self.mqtt_on_health)
  80. self._mqttc.message_callback_add(device+self.__DATATOPIC, self.mqtt_on_data)
  81. #self._mqttc.message_callback_add(device+self.__COMMANDTOPIC, self.mqtt_on_command)
  82. self._mqttc.on_connect = self.mqtt_on_connect
  83. self._mqttc.on_disconnect = self.mqtt_on_disconnect
  84. #self._mqttc.on_subscribe = self.mqtt_on_subscribe
  85. #self._mqttc.on_unsubscribe = self.mqtt_on_unsubscribe
  86. if self.__mqttDebug:
  87. self._mqttc.on_log = self.mqtt_on_log
  88. #------------------------------------------------------------------------------
  89. # callback functions
  90. def mqtt_on_connect(self, mqttc, backend_data, flags, rc):
  91. if rc == 0:
  92. self.printInfo('Connected to %s: %s' % (mqttc._host, mqttc._port))
  93. self.networkStatus.emit("Connected to %s: %s" % (mqttc._host, mqttc._port))
  94. else:
  95. self.printInfo('rc: ' + str(rc))
  96. self.networkStatus.emit('Connection Failure (rc: ' +str(rc))
  97. self.printLine()
  98. def mqtt_on_disconnect(self, mqttc, backend_data, rc):
  99. if self.__debug:
  100. if rc != 0:
  101. self.printError("Client disconnected and its a mystery why!")
  102. self.networkStatus.emit("Uh No! WSC was disconnected!")
  103. else:
  104. self.printInfo("Client successfully disconnected.")
  105. self.networkStatus.emit("Uh No! WSC was disconnected!")
  106. self.printLine()
  107. def mqtt_on_log(self, mqttc, backend_data, level, string):
  108. print(string)
  109. self.printLine()
  110. def mqtt_on_data(self, mqttc, backend_data, msg):
  111. self.printInfo("Data Message")
  112. self.printLine()
  113. try:
  114. self._dataParser.ParseFromString(msg.payload)
  115. print("GPIO States: " + bin(self._dataParser.button))
  116. self.keyEmulator.emulateKey( self._parserTools.getModuleIDfromTopic(msg.topic),self._dataParser.button)
  117. except Exception as ex:
  118. self.printError("Failure to parse message")
  119. if self.__debug:
  120. print("Raw Message: %s" %msg.payload)
  121. template = "An exception of type {0} occured. Arguments:\n{1!r}"
  122. message = template.format(type(ex).__name__, ex.args)
  123. print(message)
  124. self.printLine()
  125. def mqtt_on_health(self, mqttc, backend_data, msg):
  126. self.printInfo("Health Message")
  127. self.printLine()
  128. try:
  129. self._healthParser.ParseFromString(msg.payload)
  130. macID = self._parserTools.macToString(self._healthParser.module_id)
  131. if self._healthParser.alive:
  132. temp_list = []
  133. for field in self._healthParser.ListFields():
  134. temp_list.append((field[0].name, field[1]))
  135. temp_list.append(('time', time.time()))
  136. self.encoders[macID] = collections.OrderedDict(temp_list)
  137. self.encoderUpdate.emit(self.getDevices())
  138. else:
  139. try:
  140. self.encoders.pop(macID)
  141. self.encoderUpdate.emit()
  142. except KeyError:
  143. self.printError("Encoder ID " +macID+" is not stored")
  144. if self.__debug:
  145. for encoder, fields in zip(self.encoders.keys(), self.encoders.values()):
  146. print(str(encoder) +" : "+ str(fields))
  147. self.printLine()
  148. except:
  149. self.printError("Error: Failure to parse message")
  150. if self.__debug:
  151. print("Raw Message: %s" %msg.payload)
  152. self.printLine()
  153. try:
  154. self.encoders.pop(msg.topic.split('/')[2])
  155. self.encoderUpdate.emit(self.getDevices())
  156. except:
  157. print("This is a fucking joke anyway")
  158. #----------------------------------------------thy--------------------------------
  159. # General API Calls
  160. def cmdStartWorkstationClient(self, ip="server.ideasX.tech", port=1883, keepAlive=60):
  161. self.ip = ip
  162. self.port = port
  163. self.keepAlive = keepAlive
  164. self.printLine()
  165. self.printInfo("Starting Workstation Client (WSC)")
  166. self.printLine()
  167. try:
  168. self._mqttc.connect(self.ip, self.port, self.keepAlive) # connect to broker
  169. for device in self.__DEVICETYPE:
  170. self._mqttc.subscribe(device + self.__HEALTHTOPIC, 1)
  171. self._mqttc.loop_forever() # need to use blocking loop otherwise python will kill process
  172. except:
  173. self.printError("There was a fucking mistake here.")
  174. sys.exit(1)
  175. def guiStartWorkstationClient(self, ip=None, port=1883, keepAlive=60):
  176. self.keepAlive = keepAlive
  177. if ip == None:
  178. settings = QSettings(self.__org, self.__app)
  179. settings.beginGroup('Broker')
  180. self.ip = settings.value('NetworkBroker', 'ideasx.duckdns.org')
  181. self.port = settings.value('NetworkPort', 1883)
  182. #self.__LocalBroker = settings.value('LocalBroker', '10.42.0.1')
  183. self.__LocalPort = settings.value('LocalPort', 1883)
  184. settings.endGroup()
  185. else:
  186. self.printLine()
  187. self.printInfo("Loading hardcoded defaults")
  188. self.printLine()
  189. self.ip = ip
  190. self.port = port
  191. self.printLine()
  192. self.printInfo("Starting Workstation Client (WSC)")
  193. self.printLine()
  194. try:
  195. self._mqttc.connect(self.ip, int(self.port), self.keepAlive)
  196. for device in self.__DEVICETYPE:
  197. self._mqttc.subscribe(device + self.__HEALTHTOPIC, 0)
  198. self._mqttc.subscribe(device + self.__DATATOPIC, 0)
  199. self._mqttc.loop_start() # start MQTT Client Thread
  200. except:
  201. self.printError("There was a fucking mistake here.")
  202. self.networkStatus.emit("Oh-no! Broker settings are incorrect or there is a network failure")
  203. # sys.exit(1)
  204. def guiRestartWSC(self):
  205. self.killWSC()
  206. self.networkUpdate.emit("Restarting WSC...")
  207. self.guiStartWorkstationClient()
  208. def restartWSC(self):
  209. self.printInfo("This really doesn't do anything")
  210. def killWSC(self):
  211. self._mqttc.loop_stop()
  212. self.printInfo("Murdered MQTT thread.")
  213. def getDevices(self):
  214. return self.encoders
  215. def activateEncoder(self, deviceMACAddress, deviceType=None):
  216. '''
  217. Subscribe to device's data topic and send activate command if device
  218. is not active.
  219. * Currently does not confirm subscribe is successful
  220. * Currently does not send the activate command as it does not exist
  221. deviceType = str
  222. deviceMACAddress = str(MAC_ID)
  223. '''
  224. if deviceMACAddress in self.encoders.keys():
  225. if deviceType == None:
  226. deviceDataTopic = self.__DEVICETYPE[0] + deviceMACAddress + self.__DATATOPIC
  227. else:
  228. deviceDataTopic = deviceType + deviceMACAddress + self.__DATATOPIC
  229. self._mqttc.subscribe(deviceDataTopic, 1)
  230. self.subscribedEncoders.append(deviceMACAddress)
  231. if self.__debug:
  232. self.printInfo("Device " + deviceMACAddress + " data topic was subscribed")
  233. else:
  234. self.printError("Device " + deviceMACAddress + " is not currently in the IdeasX system.")
  235. def deactivateEncoder(self, deviceMACAddress, deviceType=None, forceAction=False):
  236. '''
  237. Unsubscribe from device's data topic and send deactive command if no other WSC are using device.
  238. * Currently does not confirm unsubscribe is successful
  239. * Currently does not send the deactive command as it does not exist and I don't know how to sync that shit.
  240. '''
  241. if (deviceMACAddress in self.encoders.keys()) or (forceAction):
  242. if deviceType == None:
  243. deviceDataTopic = self.__DEVICETYPE[0] + deviceMACAddress + self.__DATATOPIC
  244. else:
  245. deviceDataTopic = deviceType + deviceMACAddress + self.__DATATOPIC
  246. self._mqttc.unsubscribe(deviceDataTopic)
  247. self.subscribedEncoders.remove(deviceMACAddress)
  248. if self.__debug:
  249. self.printInfo("Device " + deviceMACAddress + " data topic was unsubscribed")
  250. else:
  251. self.printError("Device " + deviceMACAddress + " is not currently in the IdeasX System")
  252. def shutdownDevice(self, deviceMACAddress, deviceType=None):
  253. self._commandParser.command = self._commandParser.SHUT_DOWN
  254. self._mqttc.publish(self.__DEVICETYPE[0][:-1] + deviceMACAddress + self.__COMMANDTOPIC,
  255. self._commandParser.SerializeToString().decode('utf-8') ,
  256. qos=1,
  257. retain=False)
  258. self.networkUpdate.emit("Send shutdown command to Encoder " + deviceMACAddress)
  259. self.printInfo("Send Shutdown Command to Encoder " + deviceMACAddress)
  260. def printLine(self):
  261. print('-'*70)
  262. def printError(self, errorStr):
  263. self.__errorIndex = self.__errorIndex + 1
  264. print("WSC Error #" + str(self.__errorIndex) + ": " + errorStr)
  265. def printInfo(self, msgStr):
  266. print("WSC: " + msgStr)
  267. from pykeyboard import PyKeyboard
  268. class IdeasXKeyEmulator():
  269. def __init__(self):
  270. self.__system = sys.platform
  271. self.printInfo("Detected system is " + self.__system)
  272. self.__k = PyKeyboard()
  273. self.switchOne = 0
  274. self.switchTwo = 1
  275. self.switchAdaptive = 2
  276. self.__assignedKeys = {'default': {self.switchOne: ["1", True, 0],
  277. self.switchTwo: ["2", True, 0],
  278. self.switchAdaptive: ["3", False, 0]}}
  279. self.__activeEncoders = []
  280. def activateEncoder(self, encoder):
  281. if encoder not in self.__activeEncoders:
  282. self.__activeEncoders.append(encoder)
  283. def deactivateEncoder(self, encoder):
  284. if encoder in self.__activeEncoders:
  285. self.__activeEncoders.pop(encoder)
  286. def assignKey(self, encoder, switch, key, active=True):
  287. if switch not in [self.switchOne, self.switchTwo, self.switchAdaptive]:
  288. raise ValueError("Must be IdeasXKeyEmulator() provided switch")
  289. if encoder not in list(self.__assignedKeys.keys()):
  290. self.__assignedKeys[encoder] = self.__assignedKeys['default'].copy()
  291. print(self.__assignedKeys)
  292. self.__assignedKeys[encoder][switch] = [key, active]
  293. if active == False:
  294. self.__k.release_key(key)
  295. def getAssignedKeys(self, encoder):
  296. if encoder not in self.__assignedKeys.keys():
  297. encoder = 'default'
  298. return self.__assignedKeys[encoder]
  299. def getAssignedKey(self, encoder, switch):
  300. if encoder not in self.__assignedKeys.keys():
  301. encoder = 'default'
  302. return self.__assignedKeys[encoder][switch]
  303. def getKeyDatabase(self):
  304. return self.__assignedKeys
  305. def getDefaultKeyEntry(self):
  306. return self.__assignedKeys['default']
  307. def setKeyDatabase(self, db):
  308. self.__assignedKeys = db
  309. def emulateKey(self, encoder, buttonPayload, deviceType=None):
  310. '''
  311. This is horrible and needs to be improved
  312. '''
  313. if encoder in self.__activeEncoders or True:
  314. if encoder not in self.__assignedKeys.keys():
  315. encoder = 'default'
  316. assignedKeys = self.__assignedKeys[encoder]
  317. for switch in [self.switchOne, self.switchTwo, self.switchAdaptive]:
  318. if (buttonPayload&(1<<switch)!=0):
  319. if assignedKeys[switch][1]:
  320. self.__k.tap_key(assignedKeys[switch][0])
  321. #else:
  322. #self.__k.release_key(assignedKeys[switch][0])
  323. def printInfo(self, msg):
  324. print("EM: " + msg)
  325. #def emulatePress(self, buttonPayload):
  326. if __name__ == "__main__":
  327. Host = "ideasx.dnuckdns.org"
  328. # Host = "192.168.0.101"
  329. # Host = "10.42.0.1"
  330. Port = 1883
  331. KeepAlive = 30
  332. msgFlag = False;
  333. deviceID = None;
  334. cmdPayload = None;
  335. cmdArg = None;
  336. cmdTest = False;
  337. encodeId = '23:34'
  338. km = IdeasXKeyEmulator()
  339. km.activateEncoder(encodeId)
  340. km.emulateKey(encodeId, 1)
  341. time.sleep(0.1)
  342. km.emulateKey(encodeId, 0)
  343. time.sleep(0.1)
  344. km.emulateKey(encodeId, 2)
  345. time.sleep(0.1)
  346. km.emulateKey(encodeId, 0)
  347. time.sleep(0.1)
  348. km.emulateKey(encodeId, 4)
  349. time.sleep(0.1)
  350. km.emulateKey(encodeId, 0)
  351. # wsc = WorkstationClientClass()
  352. #
  353. # if cmdTest:
  354. # wsc.cmdStartWorkstationClient(Host, Port, KeepAlive)
  355. # else:
  356. # wsc.guiStartWorkstationClient(Host, Port, KeepAlive)
  357. # time.sleep(3)
  358. # wsc.activateEncoder('18:fe:34:f1:f2:8d')
  359. # print(wsc.subscribedEncoders)
  360. # time.sleep(2)
  361. # wsc.deactivateEncoder('18:fe:34:f1:f2:8d')
  362. # print(wsc.subscribedEncoders)
  363. # time.sleep(10)
  364. # wsc.killWSC()