import csv
import queue
import socket
import threading
from threading import Thread
"""
"""
[docs]class Subscription:
"""
Class Passed Around by data coming from Multiverse, Note most of the time will be in its own thread, so copy the data before use
"""
def __init__(self):
self._name = None
self._data = None
[docs] def Name(self):
"""
returns the name of the subscription
"""
return self._name
[docs] def Data(self):
"""
Returns a list of the data
"""
return self._data
[docs] def SetData(self, name, data):
"""
Sets the class full of information
:name: the name of the subscription
:data: a list of the data
"""
self._name = name
self._data = data
[docs]class stdcom:
"""
Threaded communication class to Multiverse, Standard Communication Class. This is a stand alone class that does not retuire
PyQt5
:Host: Name, or IP address where the Nextstep plugin is running
:Port: ServicePort where the NextStep plugin is listening
:Connected: Callback arg(Bool)
:Data: Callback, where all the subscription comes in where it changes Callback(Subscription)
:Names: Call Back, where all possible names come in Callabck(str)
:Error: Callback Callback()
"""
_Callback = None
_NCallback = None
_ECallback = None
nbrAcks = 1
isopen = False
[docs] def Start(self):
"""
Start used interally when constructed starts the thread
"""
self.conditionValue = threading.Condition()
self.conditionValue = threading.Lock()
self._t = Thread(target=self.run)
self._t.start()
def __init__(self, ipaddress, port, connectedFunction =None, Callback=None, NCallback=None, ECallback = None):
self._running = True
self.activeData = {"": []} # todo need a way to stop collecting it down higher up
self._outQueue = queue.Queue()
self._outNameList = list()
self.s = None
try:
self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
except:
if self._ECallback != None :
self._ECallback()
self.HOST = ipaddress
self.PORT = int(port)
self._Callback = Callback
self._NCallback = NCallback
self._ECallback = ECallback
self.connectedCallback = connectedFunction
try:
self.s.connect((self.HOST, self.PORT))
except OSError as e:
print("Caught exception socket.error : ", e)
self.isopen = False
self.s = None
if self._ECallback != None :
self._ECallback()
if self.s is None:
print("could not open socket")
raise ConnectionError('Represents a hidden bug, do not catch this')
else:
self.isopen = True
print("Connecting", self.HOST, ":", self.PORT)
self.Start()
if self.connectedCallback != None:
self.connectedCallback(True)
[docs] def UpDateMap(self, name, data):
"""
Internal Use for stuffing data
:name: Subscription name
:data: Date coming from Multiverse
"""
with self.conditionValue:
self.activeData.update({name: data})
[docs] def ReadValues(self, name):
"""
Reads data for a given name, returns a list of data, the data that is cached from normal running operation to subscription changes.
:name: The subscriber name
:return:
"""
data = []
toDo = []
with self.conditionValue:
if name in self.activeData:
data = self.activeData.get(name)
else:
toDo.append(name)
if len(toDo):
self.AddSubscriptions(toDo)
return data
[docs] def SetCallbacks(self, callback = None, namecallback = None):
"""
SetCallbacks sets the Data and
:callback: Data Callback( Subscription)
:namecallback: Name Callback(str)
"""
with self.conditionValue:
self._Callback = callback
self._NCallback = namecallback
[docs] def terminate(self):
"""
Stops the thread,
"""
with self.conditionValue:
self._running = False
self.s.close()
self._t.join()
print("Closing")
[docs] def NamesOn(self):
"""
Turns the names on in the Multiverse Plugin, allowing all possible names to come from Multiverse platform
"""
command = "NAMES\n"
with self.conditionValue:
try:
self.s.send(command.encode("ascii"))
except OSError as e:
print("Caught exception socket.error : ", e)
self.isopen = False
if self._ECallback != None:
self._ECallback()
[docs] def GetErrors(self):
"""
Returns the status of the socket
:return: False is connection is ok
"""
with self.conditionValue:
return self.isopen
[docs] def isConnected(self):
"""
Returns the status of the socket, same as getErrors()
:return: False is connection is ok
"""
with self.conditionValue:
return self.isopen
[docs] def RemoveSub(self, who : str):
"""
Remove a Subscription previously made
:who: the name of the subscrition
"""
with self.conditionValue:
if who in self.activeData:
command = "REMOVESUB," + who
command += "\n"
try:
self.s.send(command.encode("ascii"))
except OSError as e:
print("Caught exception socket.error : ", e)
self.isopen = False
if self._ECallback != None:
self._ECallback()
del self.activeData[who]
[docs] def Ping(self):
"""
Pings the host, will except if socket in error or not open
"""
command = "PING\n"
with self.conditionValue:
try:
self.s.send(command.encode("ascii"))
except OSError as e:
print("Ping Caught exception socket.error : ", e)
self.isopen = False
if self._ECallback != None:
self._ECallback()
[docs] def ReadData(self, who):
"""
Reads data of a subscription directly from Multiverse
:who: The name of the subscription
"""
command = "READDATA," + who
command += "\n"
with self.conditionValue:
try:
self.s.send(command.encode("ascii"))
except OSError as e:
print("Caught exception socket.error : ", e)
self.isopen = False
if self._ECallback != None:
self._ECallback()
def UpdateLocalData(self, name, data):
# Usaed internally if the subscription is designed to be local
sub = Subscription()
sub.SetData(name, data)
with self.conditionValue:
CB = self._Callback
if CB == None:
self._outQueue.put(sub)
if CB:
CB(sub)
[docs] def UpdateAsInt(self, who, what, index = 0):
"""
Updates Data as int, to Multiverse no need for caller to recast to int.. ~/name will assume internal subscription does not participate with Multiverse
:who: Name of the subscription
:what: list[int]
:where: to begin to change into the array,
"""
if str(who).startswith('~/'):
self.UpdateLocalData(who, what)
else:
command = "UPDATEI," + who + "," + str(index)
for word in what:
command += "," + str(word)
command += "\n"
with self.conditionValue:
try:
self.s.send(command.encode("ascii"))
except OSError as e:
print("Caught exception socket.error : ", e)
self.isopen = False
if self._ECallback != None:
self._ECallback()
[docs] def UpdateAsBool(self, who, what, index):
"""
Updates Data as Bool, to Multiverse no need for caller to recast to Bool.. ~/name will assume internal subscription does not participate with Multiverse
:who: Name of the subscription
:what: list[Bool]
:where: to begin to change into the array,
"""
if str(who).startswith('~/'):
self.UpdateLocalData(who, what)
else:
command = "UPDATEB," + who + "," + str(index)
for word in what:
command += "," + str(word)
command += "\n"
with self.conditionValue:
try:
self.s.send(command.encode("ascii"))
except OSError as e:
print("Caught exception socket.error : ", e)
self.isopen = False
if self._ECallback != None:
self._ECallback()
[docs] def UpdateAsFloat(self, who, what, index):
"""
Updates Data as Float, to Multiverse no need for caller to recast to Float.. ~/name will assume internal subscription does not participate with Multiverse
:who: Name of the subscription
:what: list[Float]
:where: to begin to change into the array,
"""
if str(who).startswith('~/'):
self.UpdateLocalData(who, what)
else:
command = "UPDATEF," + who + "," + str(index)
for word in what:
command += "," + str(word)
command += "\n"
with self.conditionValue:
try:
self.s.send(command.encode("ascii"))
except OSError as e:
print("Caught exception socket.error : ", e)
self.isopen = False
if self._ECallback != None:
self._ECallback()
[docs] def UpdateRaw(self, who, what, index):
"""
Updates Data as when is passed, to Multiverse user should recast to desired type() ~/name will assume internal subscription does not participate with Multiverse
:who: Name of the subscription
:what: list[type()]
:where: to begin to change into the array,
"""
if len(what):
if isinstance(what[0], int):
self.UpdateAsInt(who, what, index)
return
if isinstance(what[0], float):
self.UpdateAsFloat(who, what, index)
return
if isinstance(what[0], bool):
self.UpdateAsBool(who, what, index)
return
if str(who).startswith('~/'):
self.UpdateLocalData(who, what)
else:
command = "UPDATE," + who + "," + str(index)
for word in what:
command += "," + str(word)
command += "\n"
with self.conditionValue:
try:
self.s.send(command.encode("ascii"))
except OSError as e:
print("Caught exception socket.error : ", e)
self.isopen = False
if self._ECallback != None:
self._ECallback()
[docs] def GetNotifications(self):
"""
If callback are not used, user can call this and it returns changed subsriptions, Use callbacks it makes more sense
:return: list of subs that should be read if call backs are not used
"""
with self.conditionValue:
out = self._outQueue
self._outQueue = queue.Queue()
return out
[docs] def AddSubscriptions(self, subs):
"""
Adds a list of subscriptions
:subs: A list of str of subscriber names desired subs : list[str]
"""
data = [0]
for word in subs:
self.RemoveSub(word)
with self.conditionValue:
if word not in self.activeData:
self.activeData.update({word: data})
command = "NOTIFY," + word + "\n"
try:
self.s.send(command.encode("ascii"))
except OSError as e:
print("Caught exception socket.error : ", e)
self.isopen = False
if self._ECallback != None:
self._ECallback()
[docs] def GetNames(self):
"""
Gets the names all possible names possible to subscribe to
:returns: List of possible names that are available at this time to subscribe to.
"""
with self.conditionValue:
out = self._outNameList
return out
[docs] def GetAcks(self):
"""
Gets the number of Acks, used to see if the system is alive based on Pings or other actions, resets after it has been called
:returns: Number of Acks to messages, including pings, since the last time read.
"""
with self.conditionValue:
nbrAcks = self.nbrAcks;
self.nbrAcks = 1
return nbrAcks
[docs] def run(self):
"""
Internal use, thread running
"""
self.s.settimeout(1)
while self._running:
scsv = None
while self._running:
try:
c = self.s.recv(1)
except socket.error as e:
continue
jv = c.decode('ascii')
if scsv == None:
scsv = jv
else:
scsv = scsv + jv
if jv == '\n':
break
# scsv = data.decode('ascii')
if scsv == None:
continue
reader = list(csv.reader(scsv.split('\n'), delimiter=','))
l = len(reader)
if self._running and l >= 1:
row = reader[0]
if ('NAMESUP' in row[0]):
data = []
for x in range(1, len(row)):
data.append(row[x])
with self.conditionValue:
NCB = self._NCallback
if (NCB == None):
self._outNameList += data
if NCB:
NCB(data)
elif 'READDATA' in row[0]:
if len(row) > 2:
name = row[1]
data = []
for x in range(2, len(row)):
data.append(row[x])
sub = Subscription()
sub.SetData(name, data)
self.UpDateMap(name, data)
with self.conditionValue:
CB = self._Callback
if CB == 0:
self._outQueue.put(sub)
if CB:
CB(sub)
elif 'ACK' in row[0] :
with self.conditionValue:
self.nbrAcks = self.nbrAcks + 1
self.s.close()
cBridge = None
if __name__ == "__main__":
from time import sleep
import sys
if "--version" in sys.argv :
print("1.2.3")
sys.exit()
def _AddNames(lnames):
for i in range(0, len(lnames)):
item = str(lnames[i])
print(item)
print("Press Enter to exit...")
def _AddValues(sub):
name = sub.Name()
data = sub.Data()
for each in data:
print(">>>>>> ", data)
print("Press Enter to exit...")
# this could be argv but we will weld it for now
XHOST = "192.168.199.7" # we talk to ourself
XPORT = int(4897) # service port for NextStep plugin
XSUBS = ["Scanner1.moisture", "Scannner1.basiswt"]
s1 = [11110, 0, 10, 100]
try:
cBridge = stdcom(XHOST,
XPORT) # start the ball rolling make the communication class if it fails we will die a death in lib
cBridge.SetCallbacks(_AddValues, _AddNames)
cBridge.NamesOn() # tell the communication class to turn all the names so we can get every name across Multiverse
cBridge.AddSubscriptions(XSUBS) # add our subscriptions to multiverse
for i in range(0, len(XSUBS)):
cBridge.UpdateAsFloat(XSUBS[i], s1, 0)
except:
print("Check IP or Ensure NextStep Plugin is running")
sleep(1)
cBridge.terminate()