indipyserver 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- indipyserver/__init__.py +9 -0
- indipyserver/exdriver.py +314 -0
- indipyserver/ipyserver.py +704 -0
- indipyserver/remote.py +429 -0
- indipyserver-0.0.1.dist-info/METADATA +85 -0
- indipyserver-0.0.1.dist-info/RECORD +8 -0
- indipyserver-0.0.1.dist-info/WHEEL +4 -0
- indipyserver-0.0.1.dist-info/licenses/LICENSE +21 -0
indipyserver/__init__.py
ADDED
indipyserver/exdriver.py
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
|
|
2
|
+
import asyncio, copy
|
|
3
|
+
|
|
4
|
+
import xml.etree.ElementTree as ET
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# All xml data sent from the driver should be contained in one of the following tags
|
|
11
|
+
TAGS = (b'message',
|
|
12
|
+
b'delProperty',
|
|
13
|
+
b'defSwitchVector',
|
|
14
|
+
b'setSwitchVector',
|
|
15
|
+
b'defLightVector',
|
|
16
|
+
b'setLightVector',
|
|
17
|
+
b'defTextVector',
|
|
18
|
+
b'setTextVector',
|
|
19
|
+
b'defNumberVector',
|
|
20
|
+
b'setNumberVector',
|
|
21
|
+
b'defBLOBVector',
|
|
22
|
+
b'setBLOBVector',
|
|
23
|
+
b'getProperties' # for snooping
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Note these are strings, as they are used for checking xmldata.tag values
|
|
27
|
+
|
|
28
|
+
DEFTAGS = ( 'defSwitchVector',
|
|
29
|
+
'defLightVector',
|
|
30
|
+
'defTextVector',
|
|
31
|
+
'defNumberVector',
|
|
32
|
+
'defBLOBVector'
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
NEWTAGS = ('newTextVector',
|
|
37
|
+
'newNumberVector',
|
|
38
|
+
'newSwitchVector',
|
|
39
|
+
'newBLOBVector'
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# _STARTTAGS is a tuple of ( b'<defTextVector', ... ) data received will be tested to start with such a starttag
|
|
43
|
+
_STARTTAGS = tuple(b'<' + tag for tag in TAGS)
|
|
44
|
+
|
|
45
|
+
# _ENDTAGS is a tuple of ( b'</defTextVector>', ... ) data received will be tested to end with such an endtag
|
|
46
|
+
_ENDTAGS = tuple(b'</' + tag + b'>' for tag in TAGS)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ExVector:
|
|
50
|
+
|
|
51
|
+
def __init__(self, name, deftag):
|
|
52
|
+
"Object which only holds a name and vectortype"
|
|
53
|
+
self.name = name
|
|
54
|
+
self.vectortype = deftag[3:]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ExDriver:
|
|
58
|
+
|
|
59
|
+
def __init__(self, program, *args, debug_enable=False):
|
|
60
|
+
"Executes a third party indi driver, communicates by stdin stdout"
|
|
61
|
+
|
|
62
|
+
self.program = program
|
|
63
|
+
self.args = args
|
|
64
|
+
self.debug_enable = debug_enable
|
|
65
|
+
self.proc = None
|
|
66
|
+
|
|
67
|
+
# An object for communicating is set when this driver is added to the server
|
|
68
|
+
self._commsobj = None
|
|
69
|
+
|
|
70
|
+
# This dictionary will be populated with devicename:{vectorname:vector}
|
|
71
|
+
# where vector is an ExVector object
|
|
72
|
+
self.devicenames = {}
|
|
73
|
+
|
|
74
|
+
self.snoopall = False # gets set to True if it is snooping everything
|
|
75
|
+
self.snoopdevices = set() # gets set to a set of device names
|
|
76
|
+
self.snoopvectors = set() # gets set to a set of (devicename,vectorname) tuples
|
|
77
|
+
|
|
78
|
+
self._remainder = b"" # Used to store intermediate data
|
|
79
|
+
self._stop = False # Gets set to True to stop communications
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def shutdown(self):
|
|
83
|
+
self._stop = True
|
|
84
|
+
if self._commsobj is not None:
|
|
85
|
+
self._commsobj.shutdown()
|
|
86
|
+
if self.proc is not None:
|
|
87
|
+
self.proc.terminate()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def __contains__(self, item):
|
|
91
|
+
"So a devicename can easily be checked if it is in this driver"
|
|
92
|
+
return item in self.devicenames
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
async def _readdata(self, root):
|
|
96
|
+
"""Called from communications object with received xmldata
|
|
97
|
+
and sends it towards the driver"""
|
|
98
|
+
|
|
99
|
+
if self._stop:
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
if root.tag == "enableBLOB":
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
if root.tag == "getProperties":
|
|
106
|
+
await self.xml_to_ext(root)
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
devicename = root.get("device")
|
|
110
|
+
propertyname = root.get("name")
|
|
111
|
+
|
|
112
|
+
if root.tag in NEWTAGS:
|
|
113
|
+
if devicename is None:
|
|
114
|
+
# invalid
|
|
115
|
+
return
|
|
116
|
+
if devicename in self.devicenames:
|
|
117
|
+
await self.xml_to_ext(root)
|
|
118
|
+
return
|
|
119
|
+
else:
|
|
120
|
+
# not for this driver
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
# so not in NEWTAGS
|
|
124
|
+
# Check if this driver is snooping on this device/vector
|
|
125
|
+
if self.snoopall:
|
|
126
|
+
await self.xml_to_ext(root)
|
|
127
|
+
elif devicename and (devicename in self.snoopdevices):
|
|
128
|
+
await self.xml_to_ext(root)
|
|
129
|
+
elif devicename and propertyname and ((devicename, propertyname) in self.snoopvectors):
|
|
130
|
+
await self.xml_to_ext(root)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
async def xml_to_ext(self, root):
|
|
134
|
+
"Pass data to the external driver"
|
|
135
|
+
|
|
136
|
+
binarydata = ET.tostring(root)
|
|
137
|
+
|
|
138
|
+
# log the received data
|
|
139
|
+
if logger.isEnabledFor(logging.DEBUG) and self.debug_enable:
|
|
140
|
+
logger.debug(f"RX:: {binarydata.decode('utf-8')}")
|
|
141
|
+
|
|
142
|
+
binarydata += b"\n"
|
|
143
|
+
self.proc.stdin.write(binarydata)
|
|
144
|
+
await self.proc.stdin.drain()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
async def _run_tx(self):
|
|
149
|
+
"Get data from driver and send it towards the server"
|
|
150
|
+
try:
|
|
151
|
+
# get block of xml.etree.ElementTree data
|
|
152
|
+
# from self._xmlinput and append it to self.writerque
|
|
153
|
+
while not self._stop:
|
|
154
|
+
txdata = await self._xmlinput()
|
|
155
|
+
if txdata is None:
|
|
156
|
+
return
|
|
157
|
+
# get block of xml.etree.ElementTree data
|
|
158
|
+
devicename = txdata.get("device")
|
|
159
|
+
if devicename and txdata.tag in DEFTAGS:
|
|
160
|
+
# its a definition
|
|
161
|
+
if devicename not in self.devicenames:
|
|
162
|
+
self.devicenames[devicename] = {}
|
|
163
|
+
vectorname = txdata.get("name")
|
|
164
|
+
if vectorname:
|
|
165
|
+
if vectorname not in self.devicenames[devicename]:
|
|
166
|
+
# add this vector to the self.devicenames[devicename] dictionary
|
|
167
|
+
self.devicenames[devicename][vectorname] = ExVector(vectorname, txdata.tag)
|
|
168
|
+
# check for a getProperties being sent, record what is being snooped
|
|
169
|
+
if txdata.tag == "getProperties":
|
|
170
|
+
vectorname = txdata.get("name")
|
|
171
|
+
if devicename is None:
|
|
172
|
+
self.snoopall = True
|
|
173
|
+
elif vectorname is None:
|
|
174
|
+
self.snoopdevices.add(devicename)
|
|
175
|
+
else:
|
|
176
|
+
self.snoopvectors.add((devicename,vectorname))
|
|
177
|
+
|
|
178
|
+
# transmit this data towards the server
|
|
179
|
+
await self._commsobj.run_tx(txdata)
|
|
180
|
+
|
|
181
|
+
if logger.isEnabledFor(logging.DEBUG) and self.debug_enable:
|
|
182
|
+
binarydata = ET.tostring(txdata)
|
|
183
|
+
logger.debug(f"TX:: {binarydata.decode('utf-8')}")
|
|
184
|
+
|
|
185
|
+
except Exception:
|
|
186
|
+
logger.exception("Exception report from ExDriver._run_tx")
|
|
187
|
+
raise
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
async def _xmlinput(self):
|
|
191
|
+
"""get data from driver, parse it, and return it as xml.etree.ElementTree object
|
|
192
|
+
Returns None if stop flags arises"""
|
|
193
|
+
message = b''
|
|
194
|
+
messagetagnumber = None
|
|
195
|
+
while not self._stop:
|
|
196
|
+
data = await self._datainput()
|
|
197
|
+
# data is either None, or binary data ending in b">"
|
|
198
|
+
if data is None:
|
|
199
|
+
return
|
|
200
|
+
if self._stop:
|
|
201
|
+
return
|
|
202
|
+
if not message:
|
|
203
|
+
# data is expected to start with <tag, first strip any newlines
|
|
204
|
+
data = data.strip()
|
|
205
|
+
for index, st in enumerate(_STARTTAGS):
|
|
206
|
+
if data.startswith(st):
|
|
207
|
+
messagetagnumber = index
|
|
208
|
+
break
|
|
209
|
+
elif st in data:
|
|
210
|
+
# remove any data prior to a starttag
|
|
211
|
+
positionofst = data.index(st)
|
|
212
|
+
data = data[positionofst:]
|
|
213
|
+
messagetagnumber = index
|
|
214
|
+
break
|
|
215
|
+
else:
|
|
216
|
+
# data does not start with a recognised tag, so ignore it
|
|
217
|
+
# and continue waiting for a valid message start
|
|
218
|
+
continue
|
|
219
|
+
# set this data into the received message
|
|
220
|
+
message = data
|
|
221
|
+
# either further children of this tag are coming, or maybe its a single tag ending in "/>"
|
|
222
|
+
if message.endswith(b'/>'):
|
|
223
|
+
# the message is complete, handle message here
|
|
224
|
+
try:
|
|
225
|
+
root = ET.fromstring(message.decode("us-ascii"))
|
|
226
|
+
except Exception:
|
|
227
|
+
# failed to parse the message, continue at beginning
|
|
228
|
+
message = b''
|
|
229
|
+
messagetagnumber = None
|
|
230
|
+
continue
|
|
231
|
+
# xml datablock done, return it
|
|
232
|
+
return root
|
|
233
|
+
# and read either the next message, or the children of this tag
|
|
234
|
+
continue
|
|
235
|
+
# To reach this point, the message is in progress, with a messagetagnumber set
|
|
236
|
+
# keep adding the received data to message, until an endtag is reached
|
|
237
|
+
message += data
|
|
238
|
+
if message.endswith(_ENDTAGS[messagetagnumber]):
|
|
239
|
+
# the message is complete, handle message here
|
|
240
|
+
try:
|
|
241
|
+
root = ET.fromstring(message.decode("us-ascii"))
|
|
242
|
+
except Exception:
|
|
243
|
+
# failed to parse the message, continue at beginning
|
|
244
|
+
message = b''
|
|
245
|
+
messagetagnumber = None
|
|
246
|
+
continue
|
|
247
|
+
# xml datablock done, return it
|
|
248
|
+
return root
|
|
249
|
+
# so message is in progress, with a messagetagnumber set
|
|
250
|
+
# but no valid endtag received yet, so continue the loop
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
async def _datainput(self):
|
|
254
|
+
"""Waits for binary string of data ending in > from the driver
|
|
255
|
+
Returns None if stop flags arises"""
|
|
256
|
+
remainder = self._remainder
|
|
257
|
+
if b">" in remainder:
|
|
258
|
+
# This returns with binary data ending in > as long
|
|
259
|
+
# as there are > characters in self._remainder
|
|
260
|
+
binarydata, self._remainder = remainder.split(b'>', maxsplit=1)
|
|
261
|
+
binarydata += b">"
|
|
262
|
+
return binarydata
|
|
263
|
+
# As soon as there are no > characters left in self._remainder
|
|
264
|
+
# get more data from the driver
|
|
265
|
+
while not self._stop:
|
|
266
|
+
await asyncio.sleep(0)
|
|
267
|
+
indata = await self.proc.stdout.read(100)
|
|
268
|
+
if not indata:
|
|
269
|
+
await asyncio.sleep(0.02)
|
|
270
|
+
continue
|
|
271
|
+
remainder += indata
|
|
272
|
+
if b">" in indata:
|
|
273
|
+
binarydata, self._remainder = remainder.split(b'>', maxsplit=1)
|
|
274
|
+
binarydata += b">"
|
|
275
|
+
return binarydata
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
async def _run_err(self):
|
|
279
|
+
"""gets binary string of data from exdriver proc.stderr
|
|
280
|
+
and logs it to logging.error."""
|
|
281
|
+
|
|
282
|
+
while not self._stop:
|
|
283
|
+
bindata = await self.proc.stderr.readline()
|
|
284
|
+
if not bindata:
|
|
285
|
+
await asyncio.sleep(0.02)
|
|
286
|
+
continue
|
|
287
|
+
logger.error(bindata.decode('utf-8'))
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
async def asyncrun(self):
|
|
291
|
+
"Runs the external driver"
|
|
292
|
+
|
|
293
|
+
self._stop = False
|
|
294
|
+
|
|
295
|
+
try:
|
|
296
|
+
|
|
297
|
+
self.proc = await asyncio.create_subprocess_exec(self.program,
|
|
298
|
+
*self.args,
|
|
299
|
+
stdin=asyncio.subprocess.PIPE,
|
|
300
|
+
stdout=asyncio.subprocess.PIPE,
|
|
301
|
+
stderr=asyncio.subprocess.PIPE)
|
|
302
|
+
|
|
303
|
+
# wait for external program to start
|
|
304
|
+
await asyncio.sleep(0.1)
|
|
305
|
+
# send a getProperties into the driver
|
|
306
|
+
await self._readdata( ET.fromstring("""<getProperties version="1.7" />""") )
|
|
307
|
+
|
|
308
|
+
async with asyncio.TaskGroup() as tg:
|
|
309
|
+
tg.create_task( self._run_tx() ) # Get data from driver and send it towards the server
|
|
310
|
+
tg.create_task( self._run_err() ) # data from exdriver proc.stderr and logs it to logging.error
|
|
311
|
+
except Exception:
|
|
312
|
+
logger.exception("Driver shutdown")
|
|
313
|
+
finally:
|
|
314
|
+
self._stop = True
|