IdeasXWSCBackend.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  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
  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. def __init__(self, settingFile=None, clientID = None, debug=True, mqttdebug=True):
  50. super(IdeasXWSCNetworkThread, self).__init__()
  51. # Private Class Flags and Variables
  52. self.__clientID = clientID
  53. self.__settingFile = settingFile
  54. self.__debug = debug
  55. self.__mqttDebug = mqttdebug
  56. self.__errorIndex = 0
  57. self.__refreshCb = None
  58. # MQTT Topics
  59. self.__DEVICETYPE = ["/encoder/+"]
  60. self.__COMMANDTOPIC = "/command"
  61. self.__DATATOPIC = "/data"
  62. self.__HEALTHTOPIC = "/health"
  63. # Data Structure for Encoders / Actuators
  64. self.encoders = {}
  65. self.subscribedEncoders = []
  66. # IdeasX Parsers
  67. self._healthParser = IdeasXMessages.HealthMessage()
  68. self._dataParser = IdeasXMessages.DataMessage()
  69. self._commandParser = IdeasXMessages.CommandMessage()
  70. self._parserTools = ParsingTools()
  71. self.keyEmulator = IdeasXKeyEmulator()
  72. # MQTT Client Object
  73. self._mqttc = mqtt.Client(self.__clientID, clean_session=True, userdata=None, protocol='MQTTv311')
  74. # Setup Callback Functions for each device type
  75. for device in self.__DEVICETYPE:
  76. self._mqttc.message_callback_add(device+self.__HEALTHTOPIC, self.mqtt_on_health)
  77. self._mqttc.message_callback_add(device+self.__DATATOPIC, self.mqtt_on_data)
  78. #self._mqttc.message_callback_add(device+self.__COMMANDTOPIC, self.mqtt_on_command)
  79. self._mqttc.on_connect = self.mqtt_on_connect
  80. self._mqttc.on_disconnect = self.mqtt_on_disconnect
  81. #self._mqttc.on_subscribe = self.mqtt_on_subscribe
  82. #self._mqttc.on_unsubscribe = self.mqtt_on_unsubscribe
  83. if self.__mqttDebug:
  84. self._mqttc.on_log = self.mqtt_on_log
  85. #------------------------------------------------------------------------------
  86. # callback functions
  87. def mqtt_on_connect(self, mqttc, backend_data, flags, rc):
  88. if rc == 0:
  89. self.printInfo('Connected to %s: %s' % (mqttc._host, mqttc._port))
  90. self.networkStatus.emit("Connected to %s: %s" % (mqttc._host, mqttc._port))
  91. else:
  92. self.printInfo('rc: ' + str(rc))
  93. self.networkStatus.emit('Connection Failure (rc: ' +str(rc))
  94. self.printLine()
  95. def mqtt_on_disconnect(self, mqttc, backend_data, rc):
  96. if self.__debug:
  97. if rc != 0:
  98. self.printError("Client disconnected and its a mystery why!")
  99. else:
  100. self.printInfo("Client successfully disconnected.")
  101. self.printLine()
  102. def mqtt_on_log(self, mqttc, backend_data, level, string):
  103. print(string)
  104. self.printLine()
  105. def mqtt_on_data(self, mqttc, backend_data, msg):
  106. self.printInfo("Data Message")
  107. self.printLine()
  108. try:
  109. self._dataParser.ParseFromString(msg.payload)
  110. print("GPIO States: " + bin(self._dataParser.button))
  111. self.keyEmulator.emulateKey( self._parserTools.getModuleIDfromTopic(msg.topic),self._dataParser.button)
  112. except Exception as ex:
  113. self.printError("Failure to parse message")
  114. if self.__debug:
  115. print("Raw Message: %s" %msg.payload)
  116. template = "An exception of type {0} occured. Arguments:\n{1!r}"
  117. message = template.format(type(ex).__name__, ex.args)
  118. print(message)
  119. self.printLine()
  120. def mqtt_on_health(self, mqttc, backend_data, msg):
  121. self.printInfo("Health Message")
  122. self.printLine()
  123. try:
  124. self._healthParser.ParseFromString(msg.payload)
  125. macID = self._parserTools.macToString(self._healthParser.module_id)
  126. if self._healthParser.alive:
  127. temp_list = []
  128. for field in self._healthParser.ListFields():
  129. temp_list.append((field[0].name, field[1]))
  130. temp_list.append(('time', time.time()))
  131. self.encoders[macID] = collections.OrderedDict(temp_list)
  132. self.encoderUpdate.emit(self.getDevices())
  133. else:
  134. try:
  135. self.encoders.pop(macID)
  136. self.encoderUpdate.emit()
  137. except KeyError:
  138. self.printError("Encoder ID " +macID+" is not stored")
  139. if self.__debug:
  140. for encoder, fields in zip(self.encoders.keys(), self.encoders.values()):
  141. print(str(encoder) +" : "+ str(fields))
  142. self.printLine()
  143. except:
  144. self.printError("Error: Failure to parse message")
  145. if self.__debug:
  146. print("Raw Message: %s" %msg.payload)
  147. self.printLine()
  148. try:
  149. self.encoders.pop(msg.topic.split('/')[2])
  150. self.encoderUpdate.emit(self.getDevices())
  151. except:
  152. print("This is a fucking joke anyway")
  153. #----------------------------------------------thy--------------------------------
  154. # General API Calls
  155. def cmdStartWorkstationClient(self, ip="server.ideasX.tech", port=1883, keepAlive=60):
  156. self.ip = ip
  157. self.port = port
  158. self.keepAlive = keepAlive
  159. self.printLine()
  160. self.printInfo("Starting Workstation Client (WSC)")
  161. self.printLine()
  162. try:
  163. self._mqttc.connect(self.ip, self.port, self.keepAlive) # connect to broker
  164. for device in self.__DEVICETYPE:
  165. self._mqttc.subscribe(device + self.__HEALTHTOPIC, 1)
  166. self._mqttc.loop_forever() # need to use blocking loop otherwise python will kill process
  167. except:
  168. self.printError("There was a fucking mistake here.")
  169. sys.exit(1)
  170. def guiStartWorkstationClient(self, ip="server.ideasx.tech", port=1883, keepAlive=60):
  171. self.ip = ip
  172. self.port = port
  173. self.keepAlive = keepAlive
  174. self.printLine()
  175. self.printInfo("Starting Workstation Client (WSC)")
  176. self.printLine()
  177. try:
  178. self._mqttc.connect(self.ip, self.port, self.keepAlive)
  179. for device in self.__DEVICETYPE:
  180. self._mqttc.subscribe(device + self.__HEALTHTOPIC, 0)
  181. self._mqttc.subscribe(device + self.__DATATOPIC, 0)
  182. self._mqttc.loop_start() # start MQTT Client Thread
  183. except:
  184. self.printError("There was a fucking mistake here.")
  185. sys.exit(1)
  186. def restartWSC(self):
  187. self.printInfo("This really doesn't do anything")
  188. def killWSC(self):
  189. self._mqttc.loop_stop()
  190. self.printInfo("Murdered MQTT thread.")
  191. def getDevices(self):
  192. return self.encoders
  193. def activateEncoder(self, deviceMACAddress, deviceType=None):
  194. '''
  195. Subscribe to device's data topic and send activate command if device
  196. is not active.
  197. * Currently does not confirm subscribe is successful
  198. * Currently does not send the activate command as it does not exist
  199. deviceType = str
  200. deviceMACAddress = str(MAC_ID)
  201. '''
  202. if deviceMACAddress in self.encoders.keys():
  203. if deviceType == None:
  204. deviceDataTopic = self.__DEVICETYPE[0] + deviceMACAddress + self.__DATATOPIC
  205. else:
  206. deviceDataTopic = deviceType + deviceMACAddress + self.__DATATOPIC
  207. self._mqttc.subscribe(deviceDataTopic, 1)
  208. self.subscribedEncoders.append(deviceMACAddress)
  209. if self.__debug:
  210. self.printInfo("Device " + deviceMACAddress + " data topic was subscribed")
  211. else:
  212. self.printError("Device " + deviceMACAddress + " is not currently in the IdeasX system.")
  213. def deactivateEncoder(self, deviceMACAddress, deviceType=None, forceAction=False):
  214. '''
  215. Unsubscribe from device's data topic and send deactive command if no other WSC are using device.
  216. * Currently does not confirm unsubscribe is successful
  217. * Currently does not send the deactive command as it does not exist and I don't know how to sync that shit.
  218. '''
  219. if (deviceMACAddress in self.encoders.keys()) or (forceAction):
  220. if deviceType == None:
  221. deviceDataTopic = self.__DEVICETYPE[0] + deviceMACAddress + self.__DATATOPIC
  222. else:
  223. deviceDataTopic = deviceType + deviceMACAddress + self.__DATATOPIC
  224. self._mqttc.unsubscribe(deviceDataTopic)
  225. self.subscribedEncoders.remove(deviceMACAddress)
  226. if self.__debug:
  227. self.printInfo("Device " + deviceMACAddress + " data topic was unsubscribed")
  228. else:
  229. self.printError("Device " + deviceMACAddress + " is not currently in the IdeasX System")
  230. def shutdownDevice(self, deviceMACAddress, deviceType=None):
  231. self._commandParser.command = self._commandParser.SHUT_DOWN
  232. self._mqttc.publish(self.__DEVICETYPE[0][:-1] + deviceMACAddress + self.__COMMANDTOPIC,
  233. self._commandParser.SerializeToString().decode('utf-8') ,
  234. qos=1,
  235. retain=False)
  236. self.networkUpdate.emit("Send shutdown command to Encoder " + deviceMACAddress)
  237. self.printInfo("Send Shutdown Command to Encoder " + deviceMACAddress)
  238. def printLine(self):
  239. print('-'*70)
  240. def printError(self, errorStr):
  241. self.__errorIndex = self.__errorIndex + 1
  242. print("WSC Error #" + str(self.__errorIndex) + ": " + errorStr)
  243. def printInfo(self, msgStr):
  244. print("WSC: " + msgStr)
  245. from pykeyboard import PyKeyboard
  246. class IdeasXKeyEmulator():
  247. def __init__(self):
  248. self.__system = sys.platform
  249. self.printInfo("Detected system is " + self.__system)
  250. self.__k = PyKeyboard()
  251. self.switchOne = 0
  252. self.switchTwo = 1
  253. self.switchAdaptive = 2
  254. self.__assignedKeys = {'default': {self.switchOne: ["1", True, 0],
  255. self.switchTwo: ["2", True, 0],
  256. self.switchAdaptive: ["3", False, 0]}}
  257. self.__activeEncoders = []
  258. def activateEncoder(self, encoder):
  259. if encoder not in self.__activeEncoders:
  260. self.__activeEncoders.append(encoder)
  261. def deactivateEncoder(self, encoder):
  262. if encoder in self.__activeEncoders:
  263. self.__activeEncoders.pop(encoder)
  264. def assignKey(self, encoder, switch, key, active=True):
  265. if switch not in [self.switchOne, self.switchTwo, self.switchAdaptive]:
  266. raise ValueError("Must be IdeasXKeyEmulator() provided switch")
  267. if encoder not in list(self.__assignedKeys.keys()):
  268. self.__assignedKeys[encoder] = self.__assignedKeys['default'].copy()
  269. print(self.__assignedKeys)
  270. self.__assignedKeys[encoder][switch] = [key, active]
  271. if active == False:
  272. self.__k.release_key(key)
  273. def getAssignedKeys(self, encoder):
  274. if encoder not in self.__assignedKeys.keys():
  275. encoder = 'default'
  276. return self.__assignedKeys[encoder]
  277. def getAssignedKey(self, encoder, switch):
  278. if encoder not in self.__assignedKeys.keys():
  279. encoder = 'default'
  280. return self.__assignedKeys[encoder][switch]
  281. def emulateKey(self, encoder, buttonPayload, deviceType=None):
  282. '''
  283. This is horrible and needs to be improved
  284. '''
  285. if encoder in self.__activeEncoders or True:
  286. if encoder not in self.__assignedKeys.keys():
  287. encoder = 'default'
  288. assignedKeys = self.__assignedKeys[encoder]
  289. for switch in [self.switchOne, self.switchTwo, self.switchAdaptive]:
  290. if (buttonPayload&(1<<switch)!=0):
  291. if assignedKeys[switch][1]:
  292. self.__k.tap_key(assignedKeys[switch][0])
  293. #else:
  294. #self.__k.release_key(assignedKeys[switch][0])
  295. def printInfo(self, msg):
  296. print("EM: " + msg)
  297. #def emulatePress(self, buttonPayload):
  298. if __name__ == "__main__":
  299. Host = "ideasx.dnuckdns.org"
  300. # Host = "192.168.0.101"
  301. # Host = "10.42.0.1"
  302. Port = 1883
  303. KeepAlive = 30
  304. msgFlag = False;
  305. deviceID = None;
  306. cmdPayload = None;
  307. cmdArg = None;
  308. cmdTest = False;
  309. encodeId = '23:34'
  310. km = IdeasXKeyEmulator()
  311. km.activateEncoder(encodeId)
  312. km.emulateKey(encodeId, 1)
  313. time.sleep(0.1)
  314. km.emulateKey(encodeId, 0)
  315. time.sleep(0.1)
  316. km.emulateKey(encodeId, 2)
  317. time.sleep(0.1)
  318. km.emulateKey(encodeId, 0)
  319. time.sleep(0.1)
  320. km.emulateKey(encodeId, 4)
  321. time.sleep(0.1)
  322. km.emulateKey(encodeId, 0)
  323. # wsc = WorkstationClientClass()
  324. #
  325. # if cmdTest:
  326. # wsc.cmdStartWorkstationClient(Host, Port, KeepAlive)
  327. # else:
  328. # wsc.guiStartWorkstationClient(Host, Port, KeepAlive)
  329. # time.sleep(3)
  330. # wsc.activateEncoder('18:fe:34:f1:f2:8d')
  331. # print(wsc.subscribedEncoders)
  332. # time.sleep(2)
  333. # wsc.deactivateEncoder('18:fe:34:f1:f2:8d')
  334. # print(wsc.subscribedEncoders)
  335. # time.sleep(10)
  336. # wsc.killWSC()