IdeasXWSCBackend.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  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. TODO: Develop logger class for print statements
  20. TODO: Fix subscribe / unsubscribe functionality
  21. TODO: Implement Queue which will send signal when subscription ACK is RX
  22. TODO: Expand exceptions to only parser not general actions
  23. '''
  24. import sys, time, collections
  25. from ParsingTools import ParsingTools
  26. from PyQt5.QtCore import QObject, pyqtSignal, QSettings
  27. import paho.mqtt.client as mqtt
  28. try:
  29. import IdeasXMessages_pb2 as IdeasXMessages
  30. except ImportError:
  31. print("The python classes for IdeasX are missing. Try running the Makefile in" +
  32. "ideasX-messages.")
  33. class IdeasXWSCNetworkThread(QObject):
  34. # define Qt signals (I don't understand why this is here)
  35. encoderUpdate = pyqtSignal([dict], name='encoderUpdate')
  36. networkStatus = pyqtSignal([str], name='networkStatus')
  37. networkUpdate = pyqtSignal([str], name='networkUpdate')
  38. settingsError = pyqtSignal([str], name='settingsError')
  39. def __init__(self, settingFile=None, clientID = None, debug=True, mqttdebug=True):
  40. super(IdeasXWSCNetworkThread, self).__init__()
  41. # Private Class Flags and Variables
  42. self.__clientID = clientID
  43. self.__settingFile = settingFile
  44. self.__debug = debug
  45. self.__mqttDebug = mqttdebug
  46. self.__errorIndex = 0
  47. self.__refreshCb = None
  48. self.__org = 'IdeasX'
  49. self.__app = 'Workstation-Client'
  50. # MQTT Topics
  51. self.__DEVICETYPE = ["/encoder/+", "/actuator/+"]
  52. self.__COMMANDTOPIC = "/command"
  53. self.__DATATOPIC = "/data"
  54. self.__HEALTHTOPIC = "/health"
  55. # Data Structure for Encoders / Actuators
  56. self.encoders = {}
  57. self.subscribedEncoders = []
  58. self.actuators = {}
  59. self.subscribedActuators = []
  60. # IdeasX Parsers
  61. self.__healthParser = IdeasXMessages.HealthMessage()
  62. self.__dataParser = IdeasXMessages.DataMessage()
  63. self.__commandParser = IdeasXMessages.CommandMessage()
  64. self.__parserTools = ParsingTools()
  65. self.keyEmulator = IdeasXKeyEmulator()
  66. # MQTT Client Object
  67. self._mqttc = mqtt.Client(self.__clientID, clean_session=True, userdata=None, protocol='MQTTv311')
  68. # Setup Callback Functions for each device type
  69. for device in self.__DEVICETYPE:
  70. self._mqttc.message_callback_add(device+self.__HEALTHTOPIC, self.mqtt_on_health)
  71. self._mqttc.message_callback_add(device+self.__DATATOPIC, self.mqtt_on_data)
  72. self._mqttc.message_callback_add(device+self.__COMMANDTOPIC, self.mqtt_on_command)
  73. self._mqttc.on_connect = self.mqtt_on_connect
  74. self._mqttc.on_disconnect = self.mqtt_on_disconnect
  75. #self._mqttc.on_subscribe = self.mqtt_on_subscribe
  76. #self._mqttc.on_unsubscribe = self.mqtt_on_unsubscribe
  77. if self.__mqttDebug:
  78. self._mqttc.on_log = self.mqtt_on_log
  79. '''
  80. MQTT Callback Functions
  81. '''
  82. def mqtt_on_connect(self, mqttc, backend_data, flags, rc):
  83. if rc == 0:
  84. self.printInfo('Connected to %s: %s' % (mqttc._host, mqttc._port))
  85. self.networkStatus.emit("Connected to %s: %s" % (mqttc._host, mqttc._port))
  86. else:
  87. self.printInfo('rc: ' + str(rc))
  88. self.networkStatus.emit('Connection Failure (rc: ' +str(rc))
  89. self.printLine()
  90. def mqtt_on_disconnect(self, mqttc, backend_data, rc):
  91. if self.__debug:
  92. if rc != 0:
  93. self.printError("Client disconnected and its a mystery why!")
  94. self.networkStatus.emit("Uh No! WSC was disconnected!")
  95. else:
  96. self.printInfo("Client successfully disconnected.")
  97. self.networkStatus.emit("Uh No! WSC was disconnected!")
  98. self.printLine()
  99. def mqtt_on_log(self, mqttc, backend_data, level, string):
  100. print(string)
  101. self.printLine()
  102. def mqtt_on_data(self, mqttc, backend_data, msg):
  103. self.printInfo("Data Message")
  104. self.printLine()
  105. try:
  106. self.__dataParser.ParseFromString(msg.payload)
  107. print("GPIO States: " + bin(self.__dataParser.button))
  108. self.keyEmulator.emulateKey( self.__parserTools.getModuleIDfromTopic(msg.topic),self.__dataParser.button)
  109. except Exception as ex:
  110. self.printError("Failure to parse message")
  111. if self.__debug:
  112. print("Raw Message: %s" %msg.payload)
  113. template = "An exception of type {0} occured. Arguments:\n{1!r}"
  114. message = template.format(type(ex).__name__, ex.args)
  115. print(message)
  116. self.printLine()
  117. def mqtt_on_health(self, mqttc, backend_data, msg):
  118. self.printInfo("Health Message")
  119. self.printLine()
  120. try:
  121. self.__healthParser.ParseFromString(msg.payload)
  122. macID = self.__parserTools.macToString(self.__healthParser.module_id)
  123. if self.__healthParser.alive:
  124. temp_list = []
  125. for field in self.__healthParser.ListFields():
  126. temp_list.append((field[0].name, field[1])) # copy health message fields in to list
  127. temp_list.append(('time', time.time())) # add time the message was RX to the end
  128. self.encoders[macID] = collections.OrderedDict(temp_list) # store into a ordered dictionary
  129. self.encoderUpdate.emit(self.getDevices()) # send signal to the GUI
  130. else:
  131. try:
  132. self.encoders.pop(macID) # if the encoder isn't alive attempt to remove from dictionary
  133. self.encoderUpdate.emit() # send signal to GUI
  134. except KeyError:
  135. self.printError("Encoder ID " +macID+" is not stored")
  136. if self.__debug:
  137. for encoder, fields in zip(self.encoders.keys(), self.encoders.values()):
  138. print(str(encoder) +" : "+ str(fields))
  139. self.printLine()
  140. except Exception as e:
  141. print(e)
  142. self.printError("Error: Failure to parse message")
  143. if self.__debug:
  144. print("Raw Message: %s" %msg.payload)
  145. self.printLine()
  146. try:
  147. deviceID = msg.topic.split('/')[2]
  148. self.encoders.pop(deviceID)
  149. self.encoderUpdate.emit(self.getDevices())
  150. self.deactivateEncoder(deviceID)
  151. except:
  152. print("This is a fucking joke anyway")
  153. def mqtt_on_command(self, mqttc, backend_data, msg):
  154. pass
  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=None, port=1883, keepAlive=60):
  171. self.keepAlive = keepAlive
  172. if ip == None:
  173. settings = QSettings(self.__org, self.__app)
  174. settings.beginGroup('Broker')
  175. self.ip = settings.value('NetworkBroker', 'ideasx.duckdns.org')
  176. self.port = settings.value('NetworkPort', 1883)
  177. #self.__LocalBroker = settings.value('LocalBroker', '10.42.0.1')
  178. self.__LocalPort = settings.value('LocalPort', 1883)
  179. settings.endGroup()
  180. else:
  181. self.printLine()
  182. self.printInfo("Loading hardcoded defaults")
  183. self.printLine()
  184. self.ip = ip
  185. self.port = port
  186. self.printLine()
  187. self.printInfo("Starting Workstation Client (WSC)")
  188. self.printLine()
  189. try:
  190. self._mqttc.connect(self.ip, int(self.port), self.keepAlive)
  191. for device in self.__DEVICETYPE:
  192. self._mqttc.subscribe(device + self.__HEALTHTOPIC, 0)
  193. self._mqttc.loop_start() # start MQTT Client Thread
  194. except:
  195. self.printError("There was a fucking mistake here.")
  196. self.networkStatus.emit("Oh-no! Broker settings are incorrect or there is a network failure")
  197. # sys.exit(1)
  198. def guiRestartWSC(self):
  199. self.killWSC()
  200. self.networkUpdate.emit("Restarting WSC...")
  201. self.guiStartWorkstationClient()
  202. def restartWSC(self):
  203. self.printInfo("This really doesn't do anything")
  204. def killWSC(self):
  205. self._mqttc.loop_stop()
  206. self.printInfo("Murdered MQTT thread.")
  207. def getDevices(self):
  208. return self.encoders
  209. def activateEncoder(self, deviceMACAddress, deviceType=None):
  210. '''
  211. Subscribe to device's data topic and send activate command if device
  212. is not active.
  213. * Currently does not confirm subscribe is successful
  214. * Currently does not send the activate command as it does not exist
  215. deviceType = str
  216. deviceMACAddress = str(MAC_ID)
  217. '''
  218. if deviceMACAddress not in self.encoders.keys():
  219. self.printError("Device " + deviceMACAddress + " is not currently in the IdeasX system.")
  220. else:
  221. if deviceType == None:
  222. deviceDataTopic = "/encoder/" + deviceMACAddress + self.__DATATOPIC
  223. else:
  224. deviceDataTopic = deviceType + deviceMACAddress + self.__DATATOPIC
  225. self._mqttc.subscribe(deviceDataTopic, 1)
  226. self.subscribedEncoders.append(deviceMACAddress)
  227. if self.__debug:
  228. self.printInfo("Device " + deviceMACAddress + " data topic was subscribed")
  229. def deactivateEncoder(self, deviceMACAddress, deviceType=None, forceAction=False):
  230. '''
  231. Unsubscribe from device's data topic and send deactive command if no other WSC are using device.
  232. * Currently does not confirm unsubscribe is successful
  233. * Currently does not send the deactive command as it does not exist and I don't know how to sync that shit.
  234. '''
  235. if (deviceMACAddress in self.encoders.keys()) or (forceAction):
  236. if deviceType == None:
  237. deviceDataTopic = self.__DEVICETYPE[0] + deviceMACAddress + self.__DATATOPIC
  238. else:
  239. deviceDataTopic = deviceType + deviceMACAddress + self.__DATATOPIC
  240. self._mqttc.unsubscribe(deviceDataTopic)
  241. self.subscribedEncoders.remove(deviceMACAddress)
  242. if self.__debug:
  243. self.printInfo("Device " + deviceMACAddress + " data topic was unsubscribed")
  244. else:
  245. self.printError("Device " + deviceMACAddress + " is not currently in the IdeasX System")
  246. def resetDevice(self, deviceMACAddress, deviceType=None):
  247. self.__commandParser.command = self.__commandParser.RESTART
  248. self._mqttc.publish(self.__DEVICETYPE[0][:-1] + deviceMACAddress + self.__COMMANDTOPIC,
  249. self.__commandParser.SerializeToString().decode('utf-8') ,
  250. qos=1,
  251. retain=False)
  252. self.networkUpdate.emit("Sent reset command to device " + deviceMACAddress)
  253. def shutdownDevice(self, deviceMACAddress, deviceType=None):
  254. self.__commandParser.command = self.__commandParser.SHUT_DOWN
  255. self._mqttc.publish(self.__DEVICETYPE[0][:-1] + deviceMACAddress + self.__COMMANDTOPIC,
  256. self.__commandParser.SerializeToString().decode('utf-8') ,
  257. qos=1,
  258. retain=False)
  259. self.networkUpdate.emit("Sent shutdown command to device " + deviceMACAddress)
  260. def updateDevice(self, deviceMACAddress, deviceType=None):
  261. self.__commandParser.command = self.__commandParser.OTA_UPDATE
  262. self._mqttc.publish(self.__DEVICETYPE[0][:-1] + deviceMACAddress + self.__COMMANDTOPIC,
  263. self.__commandParser.SerializeToString().decode('utf-8') ,
  264. qos=1,
  265. retain=False)
  266. self.networkUpdate.emit("Sent OTA update request to device " + deviceMACAddress)
  267. #def configureIMU(self, deviceMACAddress, deviceType=None):
  268. def printLine(self):
  269. print('-'*70)
  270. def printError(self, errorStr):
  271. self.__errorIndex = self.__errorIndex + 1
  272. print("WSC Error #" + str(self.__errorIndex) + ": " + errorStr)
  273. def printInfo(self, msgStr):
  274. print("WSC: " + msgStr)
  275. from pykeyboard import PyKeyboard
  276. class IdeasXKeyEmulator():
  277. def __init__(self):
  278. self.__system = sys.platform
  279. self.printInfo("Detected system is " + self.__system)
  280. self.__k = PyKeyboard()
  281. self.switchOne = 0
  282. self.switchTwo = 1
  283. self.switchAdaptive = 2
  284. self.__assignedKeys = {'default': {self.switchOne: ["1", True, 0],
  285. self.switchTwo: ["2", True, 0],
  286. self.switchAdaptive: ["3", False, 0]}}
  287. self.__activeEncoders = []
  288. def activateEncoder(self, encoder):
  289. if encoder not in self.__activeEncoders:
  290. self.__activeEncoders.append(encoder)
  291. def deactivateEncoder(self, encoder):
  292. if encoder in self.__activeEncoders:
  293. self.__activeEncoders.pop(encoder)
  294. def assignKey(self, encoder, switch, key, active=True):
  295. if switch not in [self.switchOne, self.switchTwo, self.switchAdaptive]:
  296. raise ValueError("Must be IdeasXKeyEmulator() provided switch")
  297. if encoder not in list(self.__assignedKeys.keys()):
  298. self.__assignedKeys[encoder] = self.__assignedKeys['default'].copy()
  299. print(self.__assignedKeys)
  300. self.__assignedKeys[encoder][switch] = [key, active]
  301. if active == False:
  302. self.__k.release_key(key)
  303. def getAssignedKeys(self, encoder):
  304. if encoder not in self.__assignedKeys.keys():
  305. encoder = 'default'
  306. return self.__assignedKeys[encoder]
  307. def getAssignedKey(self, encoder, switch):
  308. if encoder not in self.__assignedKeys.keys():
  309. encoder = 'default'
  310. return self.__assignedKeys[encoder][switch]
  311. def getKeyDatabase(self):
  312. return self.__assignedKeys
  313. def getDefaultKeyEntry(self):
  314. return self.__assignedKeys['default']
  315. def setKeyDatabase(self, db):
  316. self.__assignedKeys = db
  317. def emulateKey(self, encoder, buttonPayload, deviceType=None):
  318. '''
  319. This is horrible and needs to be improved
  320. '''
  321. if encoder in self.__activeEncoders or True:
  322. if encoder not in self.__assignedKeys.keys():
  323. encoder = 'default'
  324. assignedKeys = self.__assignedKeys[encoder]
  325. for switch in [self.switchOne, self.switchTwo, self.switchAdaptive]:
  326. if (buttonPayload&(1<<switch)!=0):
  327. if assignedKeys[switch][1]:
  328. self.__k.tap_key(assignedKeys[switch][0])
  329. #else:
  330. #self.__k.release_key(assignedKeys[switch][0])
  331. def printInfo(self, msg):
  332. print("EM: " + msg)
  333. #def emulatePress(self, buttonPayload):
  334. if __name__ == "__main__":
  335. Host = "ideasx.duckdns.org"
  336. # Host = "192.168.0.101"
  337. # Host = "10.42.0.1"
  338. Port = 1883
  339. KeepAlive = 30
  340. msgFlag = False;
  341. deviceID = None;
  342. cmdPayload = None;
  343. cmdArg = None;
  344. cmdTest = False;
  345. #
  346. # encodeId = '23:34'
  347. #
  348. # km = IdeasXKeyEmulator()
  349. #
  350. # km.activateEncoder(encodeId)
  351. # km.emulateKey(encodeId, 1)
  352. # time.sleep(0.1)
  353. # km.emulateKey(encodeId, 0)
  354. # time.sleep(0.1)
  355. #
  356. # km.emulateKey(encodeId, 2)
  357. # time.sleep(0.1)
  358. # km.emulateKey(encodeId, 0)
  359. # time.sleep(0.1)
  360. # km.emulateKey(encodeId, 4)
  361. # time.sleep(0.1)
  362. # km.emulateKey(encodeId, 0)
  363. wsc = IdeasXWSCNetworkThread()
  364. if cmdTest:
  365. wsc.cmdStartWorkstationClient(Host, Port, KeepAlive)
  366. else:
  367. wsc.guiStartWorkstationClient(Host, Port, KeepAlive)
  368. time.sleep(3)
  369. (result, mid) = wsc._mqttc.subscribe('/encoders/18:fe:34:d2:6f:68/health', qos=0)
  370. print(result, mid)
  371. (result, mid) = wsc._mqttc.subscribe('/encoders/18:fe:34:d2:6f:68/health', qos=0)
  372. print(result, mid)
  373. wsc.activateEncoder('18:fe:34:d2:6f:68')
  374. # print(wsc.subscribedEncoders)
  375. # time.sleep(2)
  376. # wsc.deactivateEncoder('18:fe:34:d2:6f:68')
  377. # print(wsc.subscribedEncoders)
  378. time.sleep(10)
  379. wsc.killWSC()