Browse Source

Initalization

curiousmuch 7 years ago
commit
980978b642
9 changed files with 1134 additions and 0 deletions
  1. 6 0
      .gitignore
  2. 164 0
      IdeasXDatabaseManager.py
  3. 178 0
      IdeasXWorkstationClient.py
  4. 340 0
      Qt/mainwindow.ui
  5. 40 0
      gui_example.py
  6. BIN
      icon/IDEAS.png
  7. 183 0
      mainwindow.py
  8. 1 0
      protocolbuffers/IdeasXMessages_pb2.py
  9. 222 0
      wsc_backend.py

+ 6 - 0
.gitignore

@@ -0,0 +1,6 @@
+__pycache__
+*.db
+examples
+.project
+.pydevproject
+.settings

+ 164 - 0
IdeasXDatabaseManager.py

@@ -0,0 +1,164 @@
+'''
+Title: IdeasXDatabaseManager Class
+Author: Tyler Berezowsky 
+Description: The purpose of this class is to parse the general portions 
+(non-extended) portions of HealthMessages from the IdeasX system. Parshing 
+completed by auto-generated code from the Google Protocols project. Once 
+parsed, the information is stored into an SQLite DB. 
+
+Requirements: 
+- This class needs to be expandable for different device types.
+
+ToDo: 
+- Code should be updated to utilize QSqlQuery.execBatch
+'''
+
+from PyQt5 import QtSql
+import time 
+import os
+try:     
+    from protocolbuffers import IdeasXMessages_pb2 as IdeasXMessages
+except ImportError: 
+    print("The python classes for IdeasX are missing. Try running the Makefile in" +
+            "ideasX-messages.")
+            
+
+class IdeasXDatabaseManager():
+    def __init__(self, database_filename='IdeasX.db'): 
+        ''' Attempt to initialize database.
+        '''
+        self.db = QtSql.QSqlDatabase.addDatabase('QSQLITE')
+        self.db.setDatabaseName(database_filename)
+        db_open = self.db.open()
+        # check if DB exists 
+        #if not os.path.isfile('./'+database_filename):
+        if True:    
+            if db_open:
+                self.query = QtSql.QSqlQuery() 
+                self.query.exec("CREATE TABLE encoder("                 + 
+                                    "module_id char(17) primary key, "  +
+                                    "state_of_charge int, "             +
+                                    "battery_voltage real, "            +
+                                    "rssi int, "                        +
+                                    "firmware_version int, "            + 
+                                    "hardware_version int, "            +
+                                    "ssid varchar(33), "                +
+                                    "bssid varchar(33), "               +
+                                    "active bool, "                     +
+                                    "charging bool, "                   +
+                                    "low_battery bool, "                +
+                                    "ota bool);")       
+                
+                self.printError("Created database")
+            else: 
+                self.printError("Failed to open/create database")
+                # system exit
+        else:
+            self.printError("Database already exists")
+            self.query = QtSql.QSqlQuery() 
+
+            
+        # initalize parsers
+        self.healthMessage = IdeasXMessages.HealthMessage()
+        #self.dataMessageParser = IdeasXMessages.DataMessage()
+        #self.commandMessageParser = IdeasXMessages.CommandMessage()
+            
+    def printError(self, error_msg):
+        print("IdeasX Database Manager Error: " + error_msg)
+
+    def printMsg(self, msg):
+        print("IdeasX Database Manager: " + msg)
+            
+    def parseHealthMessage(self, msg):
+        self.healthMessage.ParseFromString(msg)
+        
+        flagOTA = 0 
+        flagCharging = 0 
+        flagLb = 0 
+        flagActive = 0 
+        if self.healthMessage.state.ota: 
+            flagOTA = 1
+        if self.healthMessage.state.charging:
+            flagCharging = 1 
+        if self.healthMessage.state.lb: 
+            flagLb = 1 
+        if self.healthMessage.state.active: 
+            flagActive = 1
+            
+        update = self.query.exec("SELECT 1 FROM encoder WHERE module_id ='"+self.macToString(self.healthMessage.module_id)+"';")
+        if self.query.next():
+            update = self.query.exec_("UPDATE encoder "+ 
+                             "SET state_of_charge="+str(self.calculateSOC(self.healthMessage.soc))+","+
+                             "battery_voltage="+str(self.calculateVCell(self.healthMessage.vcell))+","+
+                             "firmware_version="+str(self.healthMessage.firmware)+","+
+                             "hardware_version="+str(self.healthMessage.hardware_version)+","+ 
+                             "rssi="+str(self.healthMessage.rssi)+","+
+                             "ssid='"+self.healthMessage.ssid+"',"+
+                             "bssid='"+self.healthMessage.bssid+"',"+
+                             "active="+str(flagActive)+","+ 
+                             "ota="+str(flagOTA)+","+ 
+                             "charging="+str(flagCharging)+","+ 
+                             "low_battery="+str(flagLb)+" "+
+                             "WHERE module_id='"+self.macToString(self.healthMessage.module_id)+"';")
+            self.printMsg("Updated existing module fields " + self.macToString(self.healthMessage.module_id))
+
+            
+        else:
+            update = self.query.exec_("INSERT INTO encoder VALUES("+
+                                     "'"+self.macToString(self.healthMessage.module_id)+"',"+
+                                     str(self.calculateSOC(self.healthMessage.soc))+","+
+                                     str(self.calculateVCell(self.healthMessage.vcell))+","+
+                                     str(self.healthMessage.rssi)+","+
+                                     str(self.healthMessage.firmware)+","+
+                                     str(self.healthMessage.hardware_version)+","+
+                                     "'"+self.healthMessage.ssid+"',"+
+                                     "'"+self.healthMessage.bssid+"',"+
+                                     str(flagActive)+","+
+                                     str(flagCharging)+","+
+                                     str(flagLb)+","+
+                                     str(flagOTA)+
+                                     ");")
+            self.printMsg("Created new module " + self.macToString(self.healthMessage.module_id))
+        if update == False:
+            self.printError("SQL operation failed!")
+    
+    def macToString(self, mac_bytes):
+        ''' Convert uint8 byte string to "XX:XX:XX:XX:XX" 
+        '''
+        mac_str = ""
+        for byte in mac_bytes: 
+            mac_str = mac_str + format(byte, 'x') + ':'
+        return mac_str[:-1].format('utf-8')
+            
+    def calculateVCell(self, raw_Vcell):
+        return raw_Vcell*1.25e-3
+    
+    def calculateSOC(self, raw_SOC):
+        return raw_SOC.to_bytes(2, 'big')[0]
+        
+
+    def clearDatabase(self):
+        self.printError("Failed to delete database")
+        
+if __name__ == '__main__':
+    dbm = IdeasXDatabaseManager()
+    msg = IdeasXMessages.HealthMessage()
+    msg.module_id = bytes("1113321", 'utf-8')
+    msg.rssi = -56 
+    msg.soc = 89 
+    msg.vcell = 54323
+    msg.ssid = "Icaraus"
+    msg.state.ota = False 
+    msg.state.lb = False 
+    msg.state.charging = False
+    msg.state.active = False
+    msg.auth = 0 
+    msg.firmware = 123
+    msg.hardware_version = 1 
+    msg.rom = 0
+    str_msg = msg.SerializeToString()
+    
+    dbm.parseHealthMessage(str_msg)
+    
+        
+            # exit program 

+ 178 - 0
IdeasXWorkstationClient.py

@@ -0,0 +1,178 @@
+'''
+Title: IdeasXWorkstationClient Class 
+Author: Tyler Berezowsky 
+Description: 
+'''
+import sys
+import os
+import getopt
+
+try:
+    import paho.mqtt.client as mqtt
+    import paho.mqtt.publish as mqtt_pub
+except ImportError:
+    # This part is only required to run the example from within the examples
+    # directory when the module itself is not installed.
+    #
+    # If you have the module installed, just use "import paho.mqtt.client"
+    import os
+    import inspect
+    cmd_subfolder = os.path.realpath(os.path.abspath(os.path.join(os.path.split(inspect.getfile( inspect.currentframe() ))[0],"../src")))
+    if cmd_subfolder not in sys.path:
+        sys.path.insert(0, cmd_subfolder)
+    import paho.mqtt.client as mqtt
+
+try:     
+    from protocolbuffers import IdeasXMessages_pb2 as IdeasXMessages
+    import IdeasXDatabaseManager 
+except ImportError: 
+    print("The python classes for IdeasX are missing. Try running the Makefile in" +
+            "ideasX-messages.")
+            
+data = []
+
+#------------------------------------------------------------------------------
+# Sniffer Client Class
+
+class IdeasXWorkstationClient(): 
+    def __init__(self):
+        self.command_topic = "/encoder/+/command"
+        self.data_topic = "/encoder/+/data"
+        self.health_topic = "/encoder/+/health"
+        self.mqttdebug = False 
+        self.debug = False
+        
+        self.dmb = IdeasXDatabaseManager.IdeasXDatabaseManager()
+        
+        self._mqttc = mqtt.Client(clean_session=True, userdata=None,
+                protocol='MQTTv311')
+        
+        self._mqttc.message_callback_add(self.health_topic, self.mqtt_on_health)
+        #self._mqttc.message_callback_add(self.data_topic, self.mqtt_on_data)
+        #self._mqttc.message_callback_add(self.command_topic, self.mqtt_on_command)       
+        self._mqttc.on_connect = self.mqtt_on_connect
+        self._mqttc.on_disconnect = self.mqtt_on_disconnect 
+        
+        if self.mqttdebug: 
+            self._mqttc.on_log = self.mqtt_on_log 
+
+#------------------------------------------------------------------------------
+# callback functions
+
+    def mqtt_on_connect(self, mqttc, backend_data, flags, rc): 
+        if rc == 0: 
+            print('Connected to %s: %s' % (mqttc._host, mqttc._port))
+        else: 
+            print('rc: ' + str(rc))
+        print('-'*70)
+
+    def mqtt_on_disconnect(self, mqttc, backend_data, rc):
+        if self.debug: 
+            if rc != 0: 
+                print("Client disconnected and its a mystery why!")
+            else: 
+                print("Client successfully disconnected.") 
+            self.print_line()            
+
+    def mqtt_on_health(self, mqttc, backend_data, msg):
+        '''
+        try: 
+            self.dmb.parseHealthMessage(msg.payload)
+        except: 
+            print("Error: Failure to parse message")
+            if self.debug:
+                print("Raw Message: %s\n" %msg.payload)
+        '''
+        self.dmb.parseHealthMessage(msg.payload)
+        self.print_line()
+        
+
+    def mqtt_on_log(self, mqttc, backend_data, level, string):
+        print(string)
+        self.print_line()
+
+
+#------------------------------------------------------------------------------
+# General API Calls 
+        
+    def startWorkstationClient(self, ip="server.ideasX.tech", port=1883, keepalive=60):     
+        self.ip = ip 
+        self.port = port 
+        self.keepalive = keepalive 
+        self._mqttc.connect(ip, port, keepalive)      # connect to broker
+        #self._mqttc.subscribe(self.command_topic, 2)
+        self._mqttc.subscribe(self.health_topic, 1)
+        #self._mqttc.subscribe(self.data_topic, 2)                              
+        self._mqttc.loop_forever() # need to use blocking loop otherwise python will kill process
+    
+            
+    def print_line(self):
+        print('-'*70)
+        
+
+    
+if __name__ == "__main__": 
+    argv = sys.argv[1:] 
+    wsc = IdeasXWorkstationClient()
+    Host = "ideasx.duckdns.org"
+    Port = 1883 
+    KeepAlive = 30
+    msgFlag = False;     
+    deviceID = None; 
+    cmdPayload = None; 
+    cmdArg = None;
+    
+    try: 
+        opts, args = getopt.getopt(argv, "d:h:k:p:t:c:o:",
+                                  ['device-id','host', 'keepalive',
+                                  'port', 'topic(s)','command', 'payload'])
+    except getopt.GetoptError as s: 
+        sys.exit(2)
+    for opt, arg in opts: 
+        if opt in ("-h", "--host", "--hostname"):
+            Host = arg
+        elif opt in ("-d", "--device-id"):
+            deviceID = arg
+        elif opt in ("-k", "--keepalive"):
+            KeepAlive = arg 
+        elif opt in ("-p", "--port"):
+            Port = arg
+        elif opt in ("-o", "--payload"):
+            cmdPayload = arg.encode('utf-8')
+        elif opt in ("-c", "--command"):
+            msgFlag = True
+            cmdArg = arg
+   
+            
+    if msgFlag:
+        if cmdArg in IdeasXMessages._COMMANDMESSAGE_COMMAND.values_by_name.keys():
+            msg = IdeasXMessages.CommandMessage(); 
+            msg.command = IdeasXMessages.CommandMessage.Command.Value(cmdArg)
+            if cmdPayload != None:
+                msg.payload = cmdPayload
+            if deviceID != None:
+                pubTopic = "/modules/"+deviceID+"/command"
+            else:
+                sys.exit(2)
+#            sc.print_line()                
+#            print("Preparing Message...")
+#            sc.print_line()
+#            print("Device ID: "+str(deviceID))            
+#            sc.print_line()            
+#            print(msg.__str__()[:-1])
+         
+            mqtt_pub.single(topic=pubTopic,
+                        payload=msg.SerializeToString().decode('utf-8'),
+                        retain = False, 
+                        qos=2,
+                        hostname=Host,
+                        port=Port)
+            wsc.print_line()
+            print("Message Sent")
+            wsc.print_line()
+            sys.exit(0)
+        else:
+            sys.exit(2)
+    else:
+        wsc.startWorkstationClient(ip = Host, port = Port, keepalive = KeepAlive)
+     

+ 340 - 0
Qt/mainwindow.ui

@@ -0,0 +1,340 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MainWindow</class>
+ <widget class="QMainWindow" name="MainWindow">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>415</width>
+    <height>459</height>
+   </rect>
+  </property>
+  <property name="sizePolicy">
+   <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+    <horstretch>0</horstretch>
+    <verstretch>0</verstretch>
+   </sizepolicy>
+  </property>
+  <property name="windowTitle">
+   <string>IdeasX Workstation Client</string>
+  </property>
+  <property name="documentMode">
+   <bool>false</bool>
+  </property>
+  <property name="dockNestingEnabled">
+   <bool>true</bool>
+  </property>
+  <property name="dockOptions">
+   <set>QMainWindow::AllowNestedDocks|QMainWindow::AllowTabbedDocks|QMainWindow::AnimatedDocks|QMainWindow::VerticalTabs</set>
+  </property>
+  <property name="unifiedTitleAndToolBarOnMac">
+   <bool>false</bool>
+  </property>
+  <widget class="QWidget" name="centralwidget">
+   <layout class="QVBoxLayout" name="verticalLayout">
+    <item>
+     <widget class="QTabWidget" name="tabWidget">
+      <property name="contextMenuPolicy">
+       <enum>Qt::DefaultContextMenu</enum>
+      </property>
+      <property name="layoutDirection">
+       <enum>Qt::LeftToRight</enum>
+      </property>
+      <property name="autoFillBackground">
+       <bool>true</bool>
+      </property>
+      <property name="tabPosition">
+       <enum>QTabWidget::West</enum>
+      </property>
+      <property name="currentIndex">
+       <number>2</number>
+      </property>
+      <property name="elideMode">
+       <enum>Qt::ElideNone</enum>
+      </property>
+      <property name="documentMode">
+       <bool>false</bool>
+      </property>
+      <property name="tabBarAutoHide">
+       <bool>true</bool>
+      </property>
+      <widget class="QWidget" name="tabEncoder">
+       <property name="maximumSize">
+        <size>
+         <width>16777215</width>
+         <height>16777215</height>
+        </size>
+       </property>
+       <attribute name="title">
+        <string>Encoders</string>
+       </attribute>
+       <layout class="QGridLayout" name="gridLayout">
+        <item row="1" column="0">
+         <spacer name="horizontalSpacer">
+          <property name="orientation">
+           <enum>Qt::Horizontal</enum>
+          </property>
+          <property name="sizeHint" stdset="0">
+           <size>
+            <width>40</width>
+            <height>20</height>
+           </size>
+          </property>
+         </spacer>
+        </item>
+        <item row="1" column="1">
+         <widget class="QLineEdit" name="searchEncoder">
+          <property name="placeholderText">
+           <string>Search for Encoder by Username or Device ID</string>
+          </property>
+          <property name="clearButtonEnabled">
+           <bool>true</bool>
+          </property>
+         </widget>
+        </item>
+        <item row="0" column="0" colspan="2">
+         <widget class="QTableView" name="tableEncoder">
+          <property name="enabled">
+           <bool>true</bool>
+          </property>
+          <property name="frameShadow">
+           <enum>QFrame::Sunken</enum>
+          </property>
+          <property name="alternatingRowColors">
+           <bool>true</bool>
+          </property>
+          <property name="showGrid">
+           <bool>false</bool>
+          </property>
+          <property name="gridStyle">
+           <enum>Qt::NoPen</enum>
+          </property>
+          <property name="sortingEnabled">
+           <bool>true</bool>
+          </property>
+          <attribute name="horizontalHeaderStretchLastSection">
+           <bool>true</bool>
+          </attribute>
+          <attribute name="verticalHeaderVisible">
+           <bool>false</bool>
+          </attribute>
+         </widget>
+        </item>
+       </layout>
+      </widget>
+      <widget class="QWidget" name="tabActuator">
+       <attribute name="title">
+        <string>Actuators</string>
+       </attribute>
+       <layout class="QGridLayout" name="gridLayout_2">
+        <item row="1" column="1">
+         <widget class="QLineEdit" name="searchActuator">
+          <property name="autoFillBackground">
+           <bool>false</bool>
+          </property>
+          <property name="frame">
+           <bool>true</bool>
+          </property>
+          <property name="placeholderText">
+           <string>Search for Actuator by Name or Device ID</string>
+          </property>
+          <property name="clearButtonEnabled">
+           <bool>true</bool>
+          </property>
+         </widget>
+        </item>
+        <item row="1" column="0">
+         <spacer name="horizontalSpacer_2">
+          <property name="orientation">
+           <enum>Qt::Horizontal</enum>
+          </property>
+          <property name="sizeHint" stdset="0">
+           <size>
+            <width>40</width>
+            <height>20</height>
+           </size>
+          </property>
+         </spacer>
+        </item>
+        <item row="0" column="0" colspan="2">
+         <widget class="QTableView" name="tableActuator">
+          <property name="enabled">
+           <bool>true</bool>
+          </property>
+          <property name="frameShadow">
+           <enum>QFrame::Plain</enum>
+          </property>
+          <property name="alternatingRowColors">
+           <bool>true</bool>
+          </property>
+          <property name="showGrid">
+           <bool>false</bool>
+          </property>
+          <property name="sortingEnabled">
+           <bool>true</bool>
+          </property>
+          <attribute name="horizontalHeaderStretchLastSection">
+           <bool>true</bool>
+          </attribute>
+          <attribute name="verticalHeaderVisible">
+           <bool>false</bool>
+          </attribute>
+         </widget>
+        </item>
+       </layout>
+      </widget>
+      <widget class="QWidget" name="tabSetting">
+       <attribute name="title">
+        <string>Settings</string>
+       </attribute>
+       <layout class="QGridLayout" name="gridLayout_4">
+        <item row="9" column="0">
+         <widget class="QLabel" name="labelPassword">
+          <property name="text">
+           <string>Password:</string>
+          </property>
+         </widget>
+        </item>
+        <item row="9" column="1" colspan="2">
+         <widget class="QLineEdit" name="wifiPassword"/>
+        </item>
+        <item row="1" column="0">
+         <widget class="QLabel" name="labelNetworkBroker">
+          <property name="text">
+           <string>Network Broker:</string>
+          </property>
+         </widget>
+        </item>
+        <item row="3" column="1" colspan="2">
+         <widget class="QLineEdit" name="localBroker">
+          <property name="placeholderText">
+           <string>URL or IP</string>
+          </property>
+         </widget>
+        </item>
+        <item row="1" column="1" colspan="2">
+         <widget class="QLineEdit" name="networkBroker">
+          <property name="placeholderText">
+           <string>URL or IP</string>
+          </property>
+         </widget>
+        </item>
+        <item row="8" column="0">
+         <widget class="QLabel" name="labelSSID">
+          <property name="text">
+           <string>SSID:</string>
+          </property>
+         </widget>
+        </item>
+        <item row="4" column="1" colspan="2">
+         <widget class="QLineEdit" name="localPort">
+          <property name="placeholderText">
+           <string>Port</string>
+          </property>
+         </widget>
+        </item>
+        <item row="8" column="1" colspan="2">
+         <widget class="QLineEdit" name="wifiSSID"/>
+        </item>
+        <item row="7" column="0">
+         <widget class="QLabel" name="labelAPSelector">
+          <property name="text">
+           <string>Wi-Fi Access Point:</string>
+          </property>
+         </widget>
+        </item>
+        <item row="2" column="1" colspan="2">
+         <widget class="QLineEdit" name="networkPort">
+          <property name="text">
+           <string/>
+          </property>
+          <property name="placeholderText">
+           <string>Port</string>
+          </property>
+         </widget>
+        </item>
+        <item row="7" column="1">
+         <widget class="QSpinBox" name="selectAP">
+          <property name="suffix">
+           <string/>
+          </property>
+          <property name="prefix">
+           <string>AP </string>
+          </property>
+          <property name="minimum">
+           <number>1</number>
+          </property>
+          <property name="maximum">
+           <number>5</number>
+          </property>
+         </widget>
+        </item>
+        <item row="6" column="0" colspan="2">
+         <spacer name="verticalSpacer">
+          <property name="orientation">
+           <enum>Qt::Vertical</enum>
+          </property>
+          <property name="sizeHint" stdset="0">
+           <size>
+            <width>20</width>
+            <height>40</height>
+           </size>
+          </property>
+         </spacer>
+        </item>
+        <item row="3" column="0">
+         <widget class="QLabel" name="labelLocalBroker">
+          <property name="text">
+           <string>Local Broker:</string>
+          </property>
+         </widget>
+        </item>
+        <item row="7" column="2">
+         <spacer name="horizontalSpacer_3">
+          <property name="orientation">
+           <enum>Qt::Horizontal</enum>
+          </property>
+          <property name="sizeHint" stdset="0">
+           <size>
+            <width>40</width>
+            <height>20</height>
+           </size>
+          </property>
+         </spacer>
+        </item>
+        <item row="10" column="2">
+         <widget class="QPushButton" name="buttonTrainDevice">
+          <property name="text">
+           <string>Train IdeasX Device</string>
+          </property>
+         </widget>
+        </item>
+        <item row="5" column="2">
+         <widget class="QPushButton" name="buttonSettings">
+          <property name="text">
+           <string>Apply Settings</string>
+          </property>
+         </widget>
+        </item>
+       </layout>
+      </widget>
+     </widget>
+    </item>
+   </layout>
+  </widget>
+  <widget class="QStatusBar" name="statusbar">
+   <property name="enabled">
+    <bool>true</bool>
+   </property>
+   <property name="toolTipDuration">
+    <number>-7</number>
+   </property>
+   <property name="layoutDirection">
+    <enum>Qt::RightToLeft</enum>
+   </property>
+  </widget>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>

+ 40 - 0
gui_example.py

@@ -0,0 +1,40 @@
+import sys
+from PyQt5 import QtCore, QtGui, QtWidgets, QtSql
+from mainwindow import Ui_MainWindow
+
+class IdeasXUI(Ui_MainWindow):
+    def __init__(self, MainWindow):
+        Ui_MainWindow.__init__(self)
+        self.setupUi(MainWindow)
+        MainWindow.setWindowIcon(QtGui.QIcon("./icon/IDEAS.png"))
+        db = QtSql.QSqlDatabase.addDatabase('QSQLITE')
+        db.setDatabaseName('IdeasX.db')
+        self.encoderModel = QtSql.QSqlTableModel()
+        delrow = -1 
+        self.encoderModel.setTable('encoder')
+        self.encoderModel.setEditStrategy(QtSql.QSqlTableModel.OnFieldChange)
+        self.encoderModel.select()
+        self.encoderModel.setHeaderData(0, QtCore.Qt.Horizontal, "Module ID")
+        self.encoderModel.setHeaderData(1, QtCore.Qt.Horizontal, "Battery Capacity")
+        self.encoderModel.setHeaderData(2, QtCore.Qt.Horizontal, "Battery Voltage")
+        self.tableEncoder.setModel(self.encoderModel)
+        
+        self.tableEncoder.resizeColumnsToContents()
+        self.statusbar.showMessage("Connected to IdeasX")
+
+if __name__ == '__main__': 
+    app = QtWidgets.QApplication(sys.argv)
+    main_window = QtWidgets.QMainWindow()
+    ui = IdeasXUI(main_window)
+    main_window.show()
+    
+    def updateTable():
+        print("tick")
+        ui.encoderModel.select()
+        ui.tableEncoder.resizeColumnsToContents()
+    
+    displayTimer = QtCore.QTimer()
+    displayTimer.timeout.connect(updateTable)
+    displayTimer.start(1000)
+        
+    sys.exit(app.exec_())

BIN
icon/IDEAS.png


+ 183 - 0
mainwindow.py

@@ -0,0 +1,183 @@
+# -*- coding: utf-8 -*-
+
+# Form implementation generated from reading ui file 'mainwindow.ui'
+#
+# Created by: PyQt5 UI code generator 5.7
+#
+# WARNING! All changes made in this file will be lost!
+
+from PyQt5 import QtCore, QtGui, QtWidgets
+
+class Ui_MainWindow(object):
+    def setupUi(self, MainWindow):
+        MainWindow.setObjectName("MainWindow")
+        MainWindow.resize(415, 459)
+        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
+        sizePolicy.setHorizontalStretch(0)
+        sizePolicy.setVerticalStretch(0)
+        sizePolicy.setHeightForWidth(MainWindow.sizePolicy().hasHeightForWidth())
+        MainWindow.setSizePolicy(sizePolicy)
+        MainWindow.setDocumentMode(False)
+        MainWindow.setDockNestingEnabled(True)
+        MainWindow.setDockOptions(QtWidgets.QMainWindow.AllowNestedDocks|QtWidgets.QMainWindow.AllowTabbedDocks|QtWidgets.QMainWindow.AnimatedDocks|QtWidgets.QMainWindow.VerticalTabs)
+        MainWindow.setUnifiedTitleAndToolBarOnMac(False)
+        self.centralwidget = QtWidgets.QWidget(MainWindow)
+        self.centralwidget.setObjectName("centralwidget")
+        self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget)
+        self.verticalLayout.setObjectName("verticalLayout")
+        self.tabWidget = QtWidgets.QTabWidget(self.centralwidget)
+        self.tabWidget.setContextMenuPolicy(QtCore.Qt.DefaultContextMenu)
+        self.tabWidget.setLayoutDirection(QtCore.Qt.LeftToRight)
+        self.tabWidget.setAutoFillBackground(True)
+        self.tabWidget.setTabPosition(QtWidgets.QTabWidget.West)
+        self.tabWidget.setElideMode(QtCore.Qt.ElideNone)
+        self.tabWidget.setDocumentMode(False)
+        self.tabWidget.setTabBarAutoHide(True)
+        self.tabWidget.setObjectName("tabWidget")
+        self.tabEncoder = QtWidgets.QWidget()
+        self.tabEncoder.setMaximumSize(QtCore.QSize(16777215, 16777215))
+        self.tabEncoder.setObjectName("tabEncoder")
+        self.gridLayout = QtWidgets.QGridLayout(self.tabEncoder)
+        self.gridLayout.setContentsMargins(0, 0, 0, 0)
+        self.gridLayout.setObjectName("gridLayout")
+        spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
+        self.gridLayout.addItem(spacerItem, 1, 0, 1, 1)
+        self.searchEncoder = QtWidgets.QLineEdit(self.tabEncoder)
+        self.searchEncoder.setClearButtonEnabled(True)
+        self.searchEncoder.setObjectName("searchEncoder")
+        self.gridLayout.addWidget(self.searchEncoder, 1, 1, 1, 1)
+        self.tableEncoder = QtWidgets.QTableView(self.tabEncoder)
+        self.tableEncoder.setEnabled(True)
+        self.tableEncoder.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.tableEncoder.setAlternatingRowColors(True)
+        self.tableEncoder.setShowGrid(False)
+        self.tableEncoder.setGridStyle(QtCore.Qt.NoPen)
+        self.tableEncoder.setSortingEnabled(True)
+        self.tableEncoder.setObjectName("tableEncoder")
+        self.tableEncoder.horizontalHeader().setStretchLastSection(True)
+        self.tableEncoder.verticalHeader().setVisible(False)
+        self.gridLayout.addWidget(self.tableEncoder, 0, 0, 1, 2)
+        self.tabWidget.addTab(self.tabEncoder, "")
+        self.tabActuator = QtWidgets.QWidget()
+        self.tabActuator.setObjectName("tabActuator")
+        self.gridLayout_2 = QtWidgets.QGridLayout(self.tabActuator)
+        self.gridLayout_2.setContentsMargins(0, 0, 0, 0)
+        self.gridLayout_2.setObjectName("gridLayout_2")
+        self.searchActuator = QtWidgets.QLineEdit(self.tabActuator)
+        self.searchActuator.setAutoFillBackground(False)
+        self.searchActuator.setFrame(True)
+        self.searchActuator.setClearButtonEnabled(True)
+        self.searchActuator.setObjectName("searchActuator")
+        self.gridLayout_2.addWidget(self.searchActuator, 1, 1, 1, 1)
+        spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
+        self.gridLayout_2.addItem(spacerItem1, 1, 0, 1, 1)
+        self.tableActuator = QtWidgets.QTableView(self.tabActuator)
+        self.tableActuator.setEnabled(True)
+        self.tableActuator.setFrameShadow(QtWidgets.QFrame.Plain)
+        self.tableActuator.setAlternatingRowColors(True)
+        self.tableActuator.setShowGrid(False)
+        self.tableActuator.setSortingEnabled(True)
+        self.tableActuator.setObjectName("tableActuator")
+        self.tableActuator.horizontalHeader().setStretchLastSection(True)
+        self.tableActuator.verticalHeader().setVisible(False)
+        self.gridLayout_2.addWidget(self.tableActuator, 0, 0, 1, 2)
+        self.tabWidget.addTab(self.tabActuator, "")
+        self.tabSetting = QtWidgets.QWidget()
+        self.tabSetting.setObjectName("tabSetting")
+        self.gridLayout_4 = QtWidgets.QGridLayout(self.tabSetting)
+        self.gridLayout_4.setContentsMargins(0, 0, 0, 0)
+        self.gridLayout_4.setObjectName("gridLayout_4")
+        self.labelPassword = QtWidgets.QLabel(self.tabSetting)
+        self.labelPassword.setObjectName("labelPassword")
+        self.gridLayout_4.addWidget(self.labelPassword, 9, 0, 1, 1)
+        self.wifiPassword = QtWidgets.QLineEdit(self.tabSetting)
+        self.wifiPassword.setObjectName("wifiPassword")
+        self.gridLayout_4.addWidget(self.wifiPassword, 9, 1, 1, 2)
+        self.labelNetworkBroker = QtWidgets.QLabel(self.tabSetting)
+        self.labelNetworkBroker.setObjectName("labelNetworkBroker")
+        self.gridLayout_4.addWidget(self.labelNetworkBroker, 1, 0, 1, 1)
+        self.localBroker = QtWidgets.QLineEdit(self.tabSetting)
+        self.localBroker.setObjectName("localBroker")
+        self.gridLayout_4.addWidget(self.localBroker, 3, 1, 1, 2)
+        self.networkBroker = QtWidgets.QLineEdit(self.tabSetting)
+        self.networkBroker.setObjectName("networkBroker")
+        self.gridLayout_4.addWidget(self.networkBroker, 1, 1, 1, 2)
+        self.labelSSID = QtWidgets.QLabel(self.tabSetting)
+        self.labelSSID.setObjectName("labelSSID")
+        self.gridLayout_4.addWidget(self.labelSSID, 8, 0, 1, 1)
+        self.localPort = QtWidgets.QLineEdit(self.tabSetting)
+        self.localPort.setObjectName("localPort")
+        self.gridLayout_4.addWidget(self.localPort, 4, 1, 1, 2)
+        self.wifiSSID = QtWidgets.QLineEdit(self.tabSetting)
+        self.wifiSSID.setObjectName("wifiSSID")
+        self.gridLayout_4.addWidget(self.wifiSSID, 8, 1, 1, 2)
+        self.labelAPSelector = QtWidgets.QLabel(self.tabSetting)
+        self.labelAPSelector.setObjectName("labelAPSelector")
+        self.gridLayout_4.addWidget(self.labelAPSelector, 7, 0, 1, 1)
+        self.networkPort = QtWidgets.QLineEdit(self.tabSetting)
+        self.networkPort.setText("")
+        self.networkPort.setObjectName("networkPort")
+        self.gridLayout_4.addWidget(self.networkPort, 2, 1, 1, 2)
+        self.selectAP = QtWidgets.QSpinBox(self.tabSetting)
+        self.selectAP.setSuffix("")
+        self.selectAP.setMinimum(1)
+        self.selectAP.setMaximum(5)
+        self.selectAP.setObjectName("selectAP")
+        self.gridLayout_4.addWidget(self.selectAP, 7, 1, 1, 1)
+        spacerItem2 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
+        self.gridLayout_4.addItem(spacerItem2, 6, 0, 1, 2)
+        self.labelLocalBroker = QtWidgets.QLabel(self.tabSetting)
+        self.labelLocalBroker.setObjectName("labelLocalBroker")
+        self.gridLayout_4.addWidget(self.labelLocalBroker, 3, 0, 1, 1)
+        spacerItem3 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
+        self.gridLayout_4.addItem(spacerItem3, 7, 2, 1, 1)
+        self.buttonTrainDevice = QtWidgets.QPushButton(self.tabSetting)
+        self.buttonTrainDevice.setObjectName("buttonTrainDevice")
+        self.gridLayout_4.addWidget(self.buttonTrainDevice, 10, 2, 1, 1)
+        self.buttonSettings = QtWidgets.QPushButton(self.tabSetting)
+        self.buttonSettings.setObjectName("buttonSettings")
+        self.gridLayout_4.addWidget(self.buttonSettings, 5, 2, 1, 1)
+        self.tabWidget.addTab(self.tabSetting, "")
+        self.verticalLayout.addWidget(self.tabWidget)
+        MainWindow.setCentralWidget(self.centralwidget)
+        self.statusbar = QtWidgets.QStatusBar(MainWindow)
+        self.statusbar.setEnabled(True)
+        self.statusbar.setLayoutDirection(QtCore.Qt.RightToLeft)
+        self.statusbar.setObjectName("statusbar")
+        MainWindow.setStatusBar(self.statusbar)
+
+        self.retranslateUi(MainWindow)
+        self.tabWidget.setCurrentIndex(1)
+        QtCore.QMetaObject.connectSlotsByName(MainWindow)
+
+    def retranslateUi(self, MainWindow):
+        _translate = QtCore.QCoreApplication.translate
+        MainWindow.setWindowTitle(_translate("MainWindow", "IdeasX Workstation Client"))
+        self.searchEncoder.setPlaceholderText(_translate("MainWindow", "Search for Encoder by Username or Device ID"))
+        self.tabWidget.setTabText(self.tabWidget.indexOf(self.tabEncoder), _translate("MainWindow", "Encoders"))
+        self.searchActuator.setPlaceholderText(_translate("MainWindow", "Search for Actuator by Name or Device ID"))
+        self.tabWidget.setTabText(self.tabWidget.indexOf(self.tabActuator), _translate("MainWindow", "Actuators"))
+        self.labelPassword.setText(_translate("MainWindow", "Password:"))
+        self.labelNetworkBroker.setText(_translate("MainWindow", "Network Broker:"))
+        self.localBroker.setPlaceholderText(_translate("MainWindow", "URL or IP"))
+        self.networkBroker.setPlaceholderText(_translate("MainWindow", "URL or IP"))
+        self.labelSSID.setText(_translate("MainWindow", "SSID:"))
+        self.localPort.setPlaceholderText(_translate("MainWindow", "Port"))
+        self.labelAPSelector.setText(_translate("MainWindow", "Wi-Fi Access Point:"))
+        self.networkPort.setPlaceholderText(_translate("MainWindow", "Port"))
+        self.selectAP.setPrefix(_translate("MainWindow", "AP "))
+        self.labelLocalBroker.setText(_translate("MainWindow", "Local Broker:"))
+        self.buttonTrainDevice.setText(_translate("MainWindow", "Train IdeasX Device"))
+        self.buttonSettings.setText(_translate("MainWindow", "Apply Settings"))
+        self.tabWidget.setTabText(self.tabWidget.indexOf(self.tabSetting), _translate("MainWindow", "Settings"))
+
+
+if __name__ == "__main__":
+    import sys
+    app = QtWidgets.QApplication(sys.argv)
+    MainWindow = QtWidgets.QMainWindow()
+    ui = Ui_MainWindow()
+    ui.setupUi(MainWindow)
+    MainWindow.show()
+    sys.exit(app.exec_())
+

+ 1 - 0
protocolbuffers/IdeasXMessages_pb2.py

@@ -0,0 +1 @@
+/home/tyler/ideasX-repositories/ideasX-messages/Python/IdeasXMessages_pb2.py

+ 222 - 0
wsc_backend.py

@@ -0,0 +1,222 @@
+#!/usr/bin/python
+
+'''
+ 
+WORKSTATION CLIENT BACKEND
+
+'''
+
+
+import sys
+try:
+    import paho.mqtt.client as mqtt
+except ImportError:
+    # This part is only required to run the example from within the examples
+    # directory when the module itself is not installed.
+    #
+    # If you have the module installed, just use "import paho.mqtt.client"
+    import os
+    import inspect
+    cmd_subfolder = os.path.realpath(os.path.abspath(os.path.join(os.path.split(inspect.getfile( inspect.currentframe() ))[0],"../src")))
+    if cmd_subfolder not in sys.path:
+        sys.path.insert(0, cmd_subfolder)
+    import paho.mqtt.client as mqtt
+# NOTE: This needs to be replaced with a QT, TKinker alternative. 
+# from wx.lib.pubsub import pub
+from packet_handling import decode_client_list, decode_data_packet
+try: 
+    from ctype_bindings import SwitchPro6
+    windows = True
+except AttributeError: 
+    print("Windows win32 API missing.\n Button presses will be still be represented in print statements.") 
+    global print_debug 
+    windows = False
+
+#------------------------------------------------------------------------------
+# WORKSTATIONCLIENT CLASS
+
+class WorkstationClientClass():
+    def __init__(self, client_id=None, debug=True, mqttdebug=False):
+        self.client_id = client_id 
+        self.debug = debug
+        self.mqttdebug = mqttdebug
+        self.mode_color = 'debug'
+            
+        self.data_topics = '/modules/+/data'
+        self.clientlist_topic = '/workstations/modulehealth'
+        self.clientlist = None 
+        self.subscribed_modules = []
+        if windows:
+            self.switchpro6 = SwitchPro6()
+        self._mqttc = mqtt.Client(self.client_id, clean_session=True, userdata=None, 
+                        protocol='MQTTv311')
+        self._mqttc.message_callback_add(self.clientlist_topic, self.mqtt_on_client_list) 
+        self._mqttc.message_callback_add(self.data_topics, self.mqtt_on_data)
+        self._mqttc.on_connect = self.mqtt_on_connect
+        self._mqttc.on_disconnect = self.mqtt_on_disconnect        
+        if self.mqttdebug: 
+            self._mqttc.on_log = self.mqtt_on_log
+            
+#------------------------------------------------------------------------------
+# callback functions
+
+    def mqtt_on_connect(self, mqttc, backend_data, flags, rc):
+        # pub.sendMessage('status.connection', status = [mqttc._host, mqttc._port, rc])
+        if self.debug: 
+            if rc == 0: 
+                print('Connected to %s: %s' % (mqttc._host, mqttc._port))
+            else: 
+                print('rc: ' + str(rc))
+
+    def mqtt_on_client_list(self, mqttc, backend_data, msg):
+        self.clientlist = decode_client_list(msg.payload) 
+        
+        for active_module_id in self.subscribed_modules: 
+            for dead_module_id in self.clientlist[self.clientlist['alive']==0]['module_id']: 
+                if active_module_id == dead_module_id: 
+                    if self.debug: 
+                        print("Conflicting ID: " + str(active_module_id)+ \
+                        "\nAttempting to kill now.")
+                    self.unsubscribe_to_module(active_module_id)
+        if self.debug: 
+            print(self.clientlist)
+        # pub.sendMessage('data.clientlist', data = self.clientlist)
+       
+        
+    def mqtt_on_data(self, mqttc, backend_data, msg):
+        #pub.sendMessage('data.module', data=msg.payload)
+        data =  decode_data_packet(msg.payload)
+        if windows:     
+            self.switchpro6.keymap_press(data[0])
+        if self.debug: 
+            print(msg.topic+" "+str(msg.qos)+" "+str(msg.payload)+" "+str(data))
+
+    def mqtt_on_log(self, mqttc, backend_data, level, string):
+        print(string)
+    
+    def mqtt_on_disconnect(self, mqttc, backend_data, rc):
+        # pub.sendMessage('status.connection', status = [None, None, rc])
+        if self.debug: 
+            if rc != 0: 
+                print("Client disconnected and its a mystery why!")
+            else: 
+                print("Client successfully disconnected.") 
+#------------------------------------------------------------------------------
+# General API Calls 
+        
+    def start_workstation_client(self, ip="127.0.0.1", port=1883, keepalive=60):     
+        self.ip = ip 
+        self.port = port 
+        self.keepalive = keepalive 
+        self.clientlist = None
+        self._mqttc.connect(ip, port, keepalive)      # connect to broker                              
+        self._mqttc.subscribe(self.clientlist_topic, 2)    # subscribe to client list 
+        self._mqttc.loop_start() # Start a thread handling network traffic   
+    
+    def subscribe_to_module(self, module_id): 
+        module_found = False
+        if type(module_id) == int: 
+            module_id = str(module_id) 
+        if len(module_id) < 8:
+            module_id = '0'*(8-len(module_id)) + module_id
+	
+        for active_module_id in self.clientlist[self.clientlist['alive']==1]['module_id']:
+            if int(module_id) == active_module_id: 
+                module_found = True
+                module_topic = '/modules/'+module_id+'/data'
+                result, mid =  self._mqttc.subscribe(module_topic, qos=0)
+                # pub.sendMessage('status.subscribe', status=[int(module_id), result, mid])
+                if result == 0:
+                    if int(module_id) not in self.subscribed_modules: 
+                        self.subscribed_modules.append(int(module_id))
+                        #pub.sendMessage('data.subscribed_modules', data=self.subscribed_modules)
+                        if self.debug: 
+                            print("Module %s was successfully added" % module_id)
+                    else: 
+                        if self.debug: 
+                            print("Module is already subscribed")
+                return result, mid
+        if module_found == False:
+            if self.debug: 
+                print("Module is not alive or in client list.") 
+                print("Module ID: " + str(module_id))
+                return 3, None 
+        
+    def unsubscribe_to_module(self, module_id): 
+        if type(module_id) == int: 
+            module_id = str(module_id) 
+        if len(module_id) < 8:
+            module_id = '0'*(8-len(module_id)) + module_id
+        
+        if int(module_id) in self.subscribed_modules: 
+            module_topic = '/modules/'+module_id+'/data'
+            result, mid = self._mqttc.unsubscribe(module_topic)
+            # pub.sendMessage('status.unsubscribe', status=[int(module_id), result, mid])
+            if result == 0: 
+                self.subscribed_modules.remove(int(module_id))
+                #pub.sendMessage('data.subscribed_modules', data=self.subscribed_modules)    # send all the subs
+                if self.debug: 
+                    print("Module %s was successfully removed" % module_id)
+            return result, mid            
+        else:            
+            if self.debug: 
+                print("Module is not subscribed") 
+                print("Subscribed Modules:\n", self.subscribed_modules)
+            return 3, None 
+
+#-----------------------------------------------------------------------------
+# Puck Connectivity 	
+# The Puck will function under the following methods. 
+# 1. The puck will make the system aware of it's presence through a health 
+#    message to the database client. There needs to be sometype of addition
+#    flag or delimiter for the database client to know it is a puck. 
+# 2. The backend needs to forward this flag via pubsub. 
+# 3. There needs to be function to pair a module to a puck with the following
+#    house keeping skills: 
+#    a. Automatically unpairs if module or puck dies 
+#    b. will not pair if puck or module is already connected to computer or 
+#       paired with another computer. 
+#    c. sends a message to puck to listen to the topic of a specific module. 
+#    d. confirms puck is listening through sometype of verification QoS = 2? 
+# 
+
+    def pair_module_to_puck(self, module_id, puck_id, pair=True):
+        if type(module_id) == int: 
+            module_id = str(module_id)
+        if len(module_id)<8: 
+            module_id = '0'*(8-len(module_id)) + module_id         
+        if type(puck_id) == int: 
+            puck_id = str(puck_id)
+        if len(puck_id)<8: 
+            puck_id = '0'*(8-len(puck_id)) + puck_id 
+        
+        module_found = False
+        # update to insure puck is alive
+        # modifiy so pandas only is loooking at modules
+        for active_module_id in self.clientlist[self.clientlist['alive']==1]['module_id']:
+            if int(module_id) == active_module_id :
+                module_found = True 
+                puck_topic = '/pucks/'+puck_id+'/command'
+                if pair:
+                    payload = 'a/modules/'+str(module_id)+'/data'
+                else: 
+                    payload = 'd/modules/'+str(module_id)+'/data'
+                result, mid =  self._mqttc.publish(puck_topic, payload, 0, retain=False)
+                if result == 0: 
+                    # self.subscribed_modules.remove(int(module_id))
+                    # pub.sendMessage('data.paired_modules', data=self.subscribed_modules)    # send all the subs
+                    if self.debug: 
+                        print("Module %s was successfully paired" % module_id)
+                    return result, mid    
+
+        if module_found == False:
+            if self.debug: 
+                print("Module is not alive or in client list.")
+                print("Module ID: " + str(module_id))
+                return 3, None 
+        
+        
+                
+if __name__ == "__main__": 
+    wsc = WorkstationClientClass() 
+    wsc.start_workstation_client(ip="server.ideasx.tech")