indipyterm 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.
- indipyterm/__init__.py +3 -0
- indipyterm/__main__.py +32 -0
- indipyterm/connections.py +660 -0
- indipyterm/devicesc.py +93 -0
- indipyterm/filechooser.py +89 -0
- indipyterm/grouppn.py +471 -0
- indipyterm/indipyterm.py +41 -0
- indipyterm/memberpn.py +598 -0
- indipyterm/startsc.py +227 -0
- indipyterm-0.0.1.dist-info/LICENSE +21 -0
- indipyterm-0.0.1.dist-info/METADATA +85 -0
- indipyterm-0.0.1.dist-info/RECORD +14 -0
- indipyterm-0.0.1.dist-info/WHEEL +4 -0
- indipyterm-0.0.1.dist-info/entry_points.txt +3 -0
indipyterm/__init__.py
ADDED
indipyterm/__main__.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import sys, argparse
|
|
4
|
+
|
|
5
|
+
from . import version
|
|
6
|
+
from .indipyterm import IPyTerm
|
|
7
|
+
from .connections import make_connection
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main():
|
|
12
|
+
"""The commandline entry point to run the terminal client."""
|
|
13
|
+
|
|
14
|
+
parser = argparse.ArgumentParser(usage="indipyterm [options]",
|
|
15
|
+
description="Terminal client to communicate to an INDI service.")
|
|
16
|
+
parser.add_argument("--port", type=int, default=7624, help="Port of the INDI server (default 7624).")
|
|
17
|
+
parser.add_argument("--host", default="localhost", help="Hostname/IP of the INDI server (default localhost).")
|
|
18
|
+
parser.add_argument("--version", action="version", version=version)
|
|
19
|
+
args = parser.parse_args()
|
|
20
|
+
|
|
21
|
+
# Create the initial server connection
|
|
22
|
+
make_connection(host=args.host, port=args.port)
|
|
23
|
+
|
|
24
|
+
# run the IPyTerm app
|
|
25
|
+
app = IPyTerm()
|
|
26
|
+
app.run()
|
|
27
|
+
|
|
28
|
+
return 0
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
if __name__ == "__main__":
|
|
32
|
+
sys.exit(main())
|
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import asyncio, queue, threading, pathlib, logging
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
|
|
7
|
+
from textual.widgets import Static, Button
|
|
8
|
+
|
|
9
|
+
from indipyclient.queclient import QueClient
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger()
|
|
12
|
+
logger.addHandler(logging.NullHandler())
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
#########################################################################
|
|
16
|
+
#
|
|
17
|
+
# Global variable _DEVICENAME will be the name of the device
|
|
18
|
+
# currently being displayed
|
|
19
|
+
#
|
|
20
|
+
#########################################################################
|
|
21
|
+
|
|
22
|
+
_DEVICENAME = ''
|
|
23
|
+
|
|
24
|
+
##########################################################################
|
|
25
|
+
#
|
|
26
|
+
# Global variable _CONNECTION will be an instance of the _Connection class
|
|
27
|
+
#
|
|
28
|
+
##########################################################################
|
|
29
|
+
|
|
30
|
+
_CONNECTION = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_devicename():
|
|
35
|
+
return _DEVICENAME
|
|
36
|
+
|
|
37
|
+
def set_devicename(devicename):
|
|
38
|
+
global _DEVICENAME
|
|
39
|
+
_DEVICENAME = devicename
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def clear_devicesc():
|
|
43
|
+
global _CONNECTION
|
|
44
|
+
_CONNECTION.devicesc = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_devicemessages(devicename=None):
|
|
48
|
+
"Returns a list of messages for the device"
|
|
49
|
+
if devicename is None:
|
|
50
|
+
devicename = get_devicename()
|
|
51
|
+
if not devicename:
|
|
52
|
+
return
|
|
53
|
+
connection = get_connection()
|
|
54
|
+
snapshot = connection.snapshot
|
|
55
|
+
if not snapshot:
|
|
56
|
+
return
|
|
57
|
+
if devicename not in snapshot:
|
|
58
|
+
return
|
|
59
|
+
messages = snapshot[devicename].messages
|
|
60
|
+
if not messages:
|
|
61
|
+
return
|
|
62
|
+
return reversed([ localtimestring(t) + " " + m for t,m in messages])
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_devicegroups(devicename=None):
|
|
66
|
+
"Returns a list of groups for the device"
|
|
67
|
+
if devicename is None:
|
|
68
|
+
devicename = get_devicename()
|
|
69
|
+
if not devicename:
|
|
70
|
+
return
|
|
71
|
+
connection = get_connection()
|
|
72
|
+
snapshot = connection.snapshot
|
|
73
|
+
if not snapshot:
|
|
74
|
+
return
|
|
75
|
+
if devicename not in snapshot:
|
|
76
|
+
return
|
|
77
|
+
device = snapshot[devicename]
|
|
78
|
+
groupset = set(vector.group for vector in device.values() if vector.enable)
|
|
79
|
+
if not groupset:
|
|
80
|
+
return
|
|
81
|
+
grouplist = list(groupset)
|
|
82
|
+
grouplist.sort()
|
|
83
|
+
return grouplist
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def make_connection(host='localhost', port=7624):
|
|
88
|
+
"Creates a singleton _Connection object"
|
|
89
|
+
global _CONNECTION
|
|
90
|
+
if _CONNECTION is None:
|
|
91
|
+
_CONNECTION = _Connection(host=host, port=port, blobfolder=None)
|
|
92
|
+
|
|
93
|
+
def get_connection():
|
|
94
|
+
return _CONNECTION
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def sendvector(vectorname, memberdict):
|
|
98
|
+
global _CONNECTION
|
|
99
|
+
global _DEVICENAME
|
|
100
|
+
if _CONNECTION is None:
|
|
101
|
+
return
|
|
102
|
+
if not _DEVICENAME:
|
|
103
|
+
return
|
|
104
|
+
if _CONNECTION.is_alive():
|
|
105
|
+
_CONNECTION.txque.put((_DEVICENAME, vectorname, memberdict))
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def sendBLOBfile(vectorname, membername, path):
|
|
109
|
+
"""create memberdict of {membername:(value, blobsize, blobformat)}"""
|
|
110
|
+
global _CONNECTION
|
|
111
|
+
global _DEVICENAME
|
|
112
|
+
if _CONNECTION is None:
|
|
113
|
+
return
|
|
114
|
+
if not _DEVICENAME:
|
|
115
|
+
return
|
|
116
|
+
if not _CONNECTION.is_alive():
|
|
117
|
+
return
|
|
118
|
+
_CONNECTION.txque.put((_DEVICENAME, vectorname, {membername:(path, 0, "")}))
|
|
119
|
+
set_last_filename(vectorname, membername, path.name)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# These two functions are used for storing a BLOB filename sent to the server
|
|
124
|
+
|
|
125
|
+
def get_last_filename(vectorname, membername):
|
|
126
|
+
"Gets a filename previously stored in the client member user_string"
|
|
127
|
+
global _CONNECTION
|
|
128
|
+
global _DEVICENAME
|
|
129
|
+
if _CONNECTION is None:
|
|
130
|
+
return
|
|
131
|
+
if not _DEVICENAME:
|
|
132
|
+
return
|
|
133
|
+
if _CONNECTION.is_alive():
|
|
134
|
+
return _CONNECTION.queclient.get_user_string(_DEVICENAME, vectorname, membername)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def set_last_filename(vectorname, membername, filename):
|
|
138
|
+
"Sets a filename into the client member user_string"
|
|
139
|
+
global _CONNECTION
|
|
140
|
+
global _DEVICENAME
|
|
141
|
+
if _CONNECTION is None:
|
|
142
|
+
return
|
|
143
|
+
if not _DEVICENAME:
|
|
144
|
+
return
|
|
145
|
+
if _CONNECTION.is_alive():
|
|
146
|
+
_CONNECTION.queclient.set_user_string(_DEVICENAME, vectorname, membername, user_string = filename)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class _ItemID():
|
|
150
|
+
|
|
151
|
+
def __init__(self):
|
|
152
|
+
self._itemdict = {}
|
|
153
|
+
self._groupdict = {}
|
|
154
|
+
# Every device, vector, widget will be given an id
|
|
155
|
+
# starting with characters 'id' followed by a string number
|
|
156
|
+
# created by incrementing this self._itemid
|
|
157
|
+
self._itemid = 0
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def set_group(self, devicename, groupname):
|
|
161
|
+
"Sets an id for a group"
|
|
162
|
+
if not devicename:
|
|
163
|
+
raise KeyError("A devicename must be given to set a group id")
|
|
164
|
+
if not groupname:
|
|
165
|
+
raise KeyError("A group name must be given to set a group id")
|
|
166
|
+
self._itemid += 1
|
|
167
|
+
self._groupdict[devicename, groupname] = self._itemid
|
|
168
|
+
return self._itemid
|
|
169
|
+
|
|
170
|
+
def get_group(self, devicename, groupname):
|
|
171
|
+
"Gets a group id, or None if one not set"
|
|
172
|
+
if not devicename:
|
|
173
|
+
raise KeyError("A devicename must be given to get a group id")
|
|
174
|
+
if not groupname:
|
|
175
|
+
raise KeyError("A group name must be given to get a group id")
|
|
176
|
+
return self._groupdict.get((devicename, groupname))
|
|
177
|
+
|
|
178
|
+
def unset_group(self, devicename, groupname):
|
|
179
|
+
if not devicename:
|
|
180
|
+
raise KeyError("A devicename must be given to unset a group id")
|
|
181
|
+
if not groupname:
|
|
182
|
+
raise KeyError("A group name must be given to unset a group id")
|
|
183
|
+
self._groupdict[devicename, groupname] = None
|
|
184
|
+
|
|
185
|
+
def set(self, devicename, vectorname=None, membername=None):
|
|
186
|
+
if not vectorname:
|
|
187
|
+
vectorname = None
|
|
188
|
+
if not membername:
|
|
189
|
+
membername = None
|
|
190
|
+
if not devicename:
|
|
191
|
+
raise KeyError("A devicename must be given to set an id")
|
|
192
|
+
if membername and (not vectorname):
|
|
193
|
+
raise KeyError("If a membername is specified, a vectorname must also be given")
|
|
194
|
+
self._itemid += 1
|
|
195
|
+
self._itemdict[devicename, vectorname, membername] = self._itemid
|
|
196
|
+
return self._itemid
|
|
197
|
+
|
|
198
|
+
def unset(self, devicename, vectorname=None, membername=None):
|
|
199
|
+
if not vectorname:
|
|
200
|
+
vectorname = None
|
|
201
|
+
if not membername:
|
|
202
|
+
membername = None
|
|
203
|
+
if not devicename:
|
|
204
|
+
raise KeyError("A devicename must be given to unset an id")
|
|
205
|
+
if membername and (not vectorname):
|
|
206
|
+
raise KeyError("If a membername is specified, a vectorname must also be given")
|
|
207
|
+
self._itemdict[devicename, vectorname, membername] = None
|
|
208
|
+
|
|
209
|
+
def get(self, devicename, vectorname=None, membername=None):
|
|
210
|
+
if not vectorname:
|
|
211
|
+
vectorname = None
|
|
212
|
+
if not membername:
|
|
213
|
+
membername = None
|
|
214
|
+
if not devicename:
|
|
215
|
+
raise KeyError("A devicename must be given to get an id")
|
|
216
|
+
if membername and (not vectorname):
|
|
217
|
+
raise KeyError("If a membername is specified, a vectorname must also be given")
|
|
218
|
+
return self._itemdict.get((devicename, vectorname, membername))
|
|
219
|
+
|
|
220
|
+
def get_devicename(self, deviceid):
|
|
221
|
+
"Given an id, get the devicename, or return None if it does not exist"
|
|
222
|
+
idnumber = int(deviceid.strip("id"))
|
|
223
|
+
for key,value in self._itemdict.items():
|
|
224
|
+
if value == idnumber:
|
|
225
|
+
return key[0]
|
|
226
|
+
|
|
227
|
+
def clear(self):
|
|
228
|
+
self._itemdict.clear()
|
|
229
|
+
self._groupdict.clear()
|
|
230
|
+
self._itemid = 0
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
##################################
|
|
234
|
+
#
|
|
235
|
+
# Create a global _ItemID
|
|
236
|
+
#
|
|
237
|
+
###################################
|
|
238
|
+
|
|
239
|
+
_ITEMID = _ItemID()
|
|
240
|
+
|
|
241
|
+
def get_id(devicename, vectorname=None, membername=None):
|
|
242
|
+
"This is imported into the gui to create ids for widgets"
|
|
243
|
+
global _ITEMID
|
|
244
|
+
idnumber = _ITEMID.get(devicename, vectorname, membername)
|
|
245
|
+
if idnumber is None:
|
|
246
|
+
return
|
|
247
|
+
return "id"+str(idnumber)
|
|
248
|
+
|
|
249
|
+
def set_id(devicename, vectorname=None, membername=None):
|
|
250
|
+
"This create ids for widgets, and returns the id"
|
|
251
|
+
global _ITEMID
|
|
252
|
+
idnumber = _ITEMID.get(devicename, vectorname, membername)
|
|
253
|
+
if idnumber is None:
|
|
254
|
+
idnumber = _ITEMID.set(devicename, vectorname, membername)
|
|
255
|
+
return "id"+str(idnumber)
|
|
256
|
+
|
|
257
|
+
def devicename_from_id(deviceid):
|
|
258
|
+
global _ITEMID
|
|
259
|
+
return _ITEMID.get_devicename(deviceid)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def get_group_id(groupname):
|
|
263
|
+
global _DEVICENAME, _ITEMID
|
|
264
|
+
if _DEVICENAME is None:
|
|
265
|
+
return
|
|
266
|
+
idnumber = _ITEMID.get_group(_DEVICENAME, groupname)
|
|
267
|
+
if idnumber is None:
|
|
268
|
+
return
|
|
269
|
+
return "gid"+str(idnumber)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def set_group_id(groupname):
|
|
273
|
+
global _DEVICENAME, _ITEMID
|
|
274
|
+
if _DEVICENAME is None:
|
|
275
|
+
return
|
|
276
|
+
idnumber = _ITEMID.get_group(_DEVICENAME, groupname)
|
|
277
|
+
if idnumber is None:
|
|
278
|
+
idnumber = _ITEMID.set_group(_DEVICENAME, groupname)
|
|
279
|
+
return "gid"+str(idnumber)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def localtimestring(t):
|
|
284
|
+
"Return a string of the local time (not date)"
|
|
285
|
+
localtime = t.astimezone(tz=None)
|
|
286
|
+
# convert microsecond to integer between 0 and 100
|
|
287
|
+
ms = localtime.microsecond//10000
|
|
288
|
+
return f"{localtime.strftime('%H:%M:%S')}.{ms:0>2d}"
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class _Connection:
|
|
292
|
+
|
|
293
|
+
def __init__(self, host=None, port=None, blobfolder=None):
|
|
294
|
+
|
|
295
|
+
# create two queues
|
|
296
|
+
# txque to transmit data
|
|
297
|
+
self.txque = queue.Queue(maxsize=4)
|
|
298
|
+
# rxque giving received data
|
|
299
|
+
self.rxque = queue.Queue(maxsize=4)
|
|
300
|
+
|
|
301
|
+
self.snapshot = None
|
|
302
|
+
self.queclient = None
|
|
303
|
+
self.clientthread = None
|
|
304
|
+
self.blobfolderpath = None
|
|
305
|
+
|
|
306
|
+
if (host == None) or (port == None):
|
|
307
|
+
self.host = None
|
|
308
|
+
self.port = None
|
|
309
|
+
self.hostport = ""
|
|
310
|
+
else:
|
|
311
|
+
self.hostport = f"{host}:{port}"
|
|
312
|
+
self.make_connection(host, port, blobfolder)
|
|
313
|
+
|
|
314
|
+
# these are filled in as the app is mounted
|
|
315
|
+
self.app = None
|
|
316
|
+
self.startsc = None
|
|
317
|
+
self.devicesc = None
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def checkhostport(self, hostport):
|
|
321
|
+
"""Given a hostport string, Checks it and sets self.hostport
|
|
322
|
+
Returns a string"""
|
|
323
|
+
hostport = hostport.strip()
|
|
324
|
+
if not hostport:
|
|
325
|
+
self.hostport = "localhost:7624"
|
|
326
|
+
return self.hostport
|
|
327
|
+
|
|
328
|
+
hostportlist = hostport.split(":")
|
|
329
|
+
if len(hostportlist) != 2:
|
|
330
|
+
host = hostportlist[0].strip()
|
|
331
|
+
if host:
|
|
332
|
+
self.hostport = host +":7624"
|
|
333
|
+
else:
|
|
334
|
+
self.hostport = "localhost:7624"
|
|
335
|
+
return self.hostport
|
|
336
|
+
host = hostportlist[0].strip()
|
|
337
|
+
port = hostportlist[1].strip()
|
|
338
|
+
if not host:
|
|
339
|
+
host = "localhost"
|
|
340
|
+
if not port.isdigit():
|
|
341
|
+
port = "7624"
|
|
342
|
+
self.hostport = f"{host}:{port}"
|
|
343
|
+
return self.hostport
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def checkblobfolder(self, blobfolder):
|
|
347
|
+
"""Given a folder, checks it, sets blobfolder, and returns the folder string
|
|
348
|
+
If the given folder is empty, removes the blobfolder
|
|
349
|
+
If the given folder is not a directory, removes the blobfolder and returns
|
|
350
|
+
Invalid Folder"""
|
|
351
|
+
if not blobfolder:
|
|
352
|
+
self.set_BLOBfolder(None)
|
|
353
|
+
return ""
|
|
354
|
+
blobfolder = pathlib.Path(blobfolder).expanduser().resolve()
|
|
355
|
+
if not blobfolder.is_dir():
|
|
356
|
+
self.set_BLOBfolder(None)
|
|
357
|
+
return "Invalid Folder"
|
|
358
|
+
self.set_BLOBfolder(blobfolder)
|
|
359
|
+
return str(blobfolder)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def set_BLOBfolder(self, blobfolder):
|
|
363
|
+
"Sets blofolder into queclient"
|
|
364
|
+
self.blobfolderpath = blobfolder
|
|
365
|
+
connection = get_connection()
|
|
366
|
+
if not connection.is_alive():
|
|
367
|
+
return
|
|
368
|
+
self.queclient.BLOBfolder = self.blobfolderpath
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def check_rxque(self) -> None:
|
|
373
|
+
"""Method to handle received data."""
|
|
374
|
+
global _ITEMID
|
|
375
|
+
|
|
376
|
+
if self.devicesc is None:
|
|
377
|
+
# there is no devicesc, so ensure startsc is active before continuing
|
|
378
|
+
if not self.startsc.is_active:
|
|
379
|
+
return
|
|
380
|
+
else:
|
|
381
|
+
# a devicesc has been set, ensure it is active before continuing
|
|
382
|
+
if not self.devicesc.is_active:
|
|
383
|
+
return
|
|
384
|
+
|
|
385
|
+
try:
|
|
386
|
+
item = self.rxque.get_nowait()
|
|
387
|
+
except queue.Empty:
|
|
388
|
+
return
|
|
389
|
+
self.rxque.task_done()
|
|
390
|
+
|
|
391
|
+
if not (self.host) or (not self.port):
|
|
392
|
+
self.snapshot = None
|
|
393
|
+
return
|
|
394
|
+
|
|
395
|
+
self.snapshot = item.snapshot
|
|
396
|
+
snapshot = self.snapshot
|
|
397
|
+
|
|
398
|
+
# system messages
|
|
399
|
+
if (item.eventtype == "Message") and (not item.devicename) and (not item.vectorname):
|
|
400
|
+
log = self.startsc.query_one("#system-messages")
|
|
401
|
+
log.clear()
|
|
402
|
+
messages = snapshot.messages
|
|
403
|
+
mlist = reversed([ localtimestring(t) + " " + m for t,m in messages ])
|
|
404
|
+
log.write_lines(mlist)
|
|
405
|
+
|
|
406
|
+
if not snapshot.connected:
|
|
407
|
+
# the connection is disconnected
|
|
408
|
+
self.clear_devices()
|
|
409
|
+
if not self.startsc.is_active:
|
|
410
|
+
self.app.push_screen('startsc')
|
|
411
|
+
return
|
|
412
|
+
|
|
413
|
+
if not item.devicename:
|
|
414
|
+
# possible getProperties or system message which is handled above, just return
|
|
415
|
+
return
|
|
416
|
+
|
|
417
|
+
# get currently displayed device
|
|
418
|
+
devicename = get_devicename()
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
if item.eventtype == "Delete":
|
|
422
|
+
if item.vectorname and snapshot[item.devicename].enable:
|
|
423
|
+
# vector is to be deleted, but the device is still enabled, so there is at least one other
|
|
424
|
+
# vector existing, and the device does not have to be deleted, just the specified vector
|
|
425
|
+
if self.devicesc and devicename and (devicename == item.devicename):
|
|
426
|
+
# This device is being displayed, remove the vector
|
|
427
|
+
vectorid = get_id(item.devicename, item.vectorname)
|
|
428
|
+
if vectorid:
|
|
429
|
+
vectorwidget = self.devicesc.query_one(f"#{vectorid}")
|
|
430
|
+
vectorwidget.remove()
|
|
431
|
+
# the delete event could include a device message
|
|
432
|
+
device = snapshot[devicename]
|
|
433
|
+
messages = device.messages
|
|
434
|
+
if messages:
|
|
435
|
+
log = self.devicesc.query_one("#device-messages")
|
|
436
|
+
log.clear()
|
|
437
|
+
mlist = reversed([ localtimestring(t) + " " + m for t,m in messages ])
|
|
438
|
+
log.write_lines(mlist)
|
|
439
|
+
|
|
440
|
+
# vector removed, does its group need to be removed?
|
|
441
|
+
groupset = set(vector.group for vector in device.values() if vector.enable)
|
|
442
|
+
# get the group of the deleted vector
|
|
443
|
+
grp = device[item.vectorname].group
|
|
444
|
+
if grp not in groupset:
|
|
445
|
+
# the grp no longer has enabled contents, and must be removed
|
|
446
|
+
grpid = get_group_id(grp)
|
|
447
|
+
tabbedcontent = self.devicesc.query_one("#dev_groups")
|
|
448
|
+
tabbedcontent.remove_pane(grpid)
|
|
449
|
+
_ITEMID.unset_group(item.devicename, grp)
|
|
450
|
+
# delete the vector id
|
|
451
|
+
_ITEMID.unset(item.devicename, item.vectorname)
|
|
452
|
+
# give every member an empty id
|
|
453
|
+
membernamelist = list(snapshot[item.devicename][item.vectorname].keys())
|
|
454
|
+
for membername in membernamelist:
|
|
455
|
+
_ITEMID.unset(item.devicename, item.vectorname, membername)
|
|
456
|
+
else:
|
|
457
|
+
# either no vectorname, or last vector is deleted, so delete entire device
|
|
458
|
+
# when a device is deleted, the associated event message, if given, is added
|
|
459
|
+
# to the client messages
|
|
460
|
+
messages = snapshot.messages
|
|
461
|
+
if messages:
|
|
462
|
+
log = self.startsc.query_one("#system-messages")
|
|
463
|
+
log.clear()
|
|
464
|
+
mlist = reversed([ localtimestring(t) + " " + m for t,m in messages ])
|
|
465
|
+
log.write_lines(mlist)
|
|
466
|
+
# remove the device from the startsc device-pane
|
|
467
|
+
deviceid = get_id(item.devicename)
|
|
468
|
+
if deviceid:
|
|
469
|
+
device_pane = self.startsc.query_one("#device-pane")
|
|
470
|
+
device_pane.remove_children(f"#{deviceid}")
|
|
471
|
+
# clear the device, vectors and members from _ITEMID
|
|
472
|
+
_ITEMID.unset(item.devicename)
|
|
473
|
+
vectornamelist = list(snapshot[item.devicename].keys())
|
|
474
|
+
for vectorname in vectornamelist:
|
|
475
|
+
_ITEMID.unset(item.devicename, vectorname)
|
|
476
|
+
membernamelist = list(snapshot[item.devicename][vectorname].keys())
|
|
477
|
+
for membername in membernamelist:
|
|
478
|
+
_ITEMID.unset(item.devicename, vectorname, membername)
|
|
479
|
+
# If the device is currently being displayed, remove its screen and
|
|
480
|
+
# drop to startsc
|
|
481
|
+
if devicename == item.devicename:
|
|
482
|
+
if not self.startsc.is_active:
|
|
483
|
+
self.app.push_screen('startsc')
|
|
484
|
+
return
|
|
485
|
+
|
|
486
|
+
# add device to startsc on receiving a definition
|
|
487
|
+
if (item.eventtype == "Define" or item.eventtype == "DefineBLOB"):
|
|
488
|
+
# does this device have an id
|
|
489
|
+
if not get_id(item.devicename):
|
|
490
|
+
deviceid = set_id(item.devicename)
|
|
491
|
+
device_pane = self.startsc.query_one("#device-pane")
|
|
492
|
+
device_pane.remove_children("#no-devices")
|
|
493
|
+
device_pane.mount(Button(item.devicename, variant="primary", classes="devices", id=deviceid))
|
|
494
|
+
# give the vector an id
|
|
495
|
+
set_id(item.devicename, item.vectorname)
|
|
496
|
+
# give every member an id
|
|
497
|
+
membernamelist = list(snapshot[item.devicename][item.vectorname].keys())
|
|
498
|
+
for membername in membernamelist:
|
|
499
|
+
set_id(item.devicename, item.vectorname, membername)
|
|
500
|
+
elif self.devicesc and devicename and (devicename == item.devicename) and (not get_id(item.devicename, item.vectorname)):
|
|
501
|
+
# known device, which is currently being displayed, but new vector, give the vector an id
|
|
502
|
+
set_id(item.devicename, item.vectorname)
|
|
503
|
+
# give every member an id
|
|
504
|
+
membernamelist = list(snapshot[item.devicename][item.vectorname].keys())
|
|
505
|
+
for membername in membernamelist:
|
|
506
|
+
set_id(item.devicename, item.vectorname, membername)
|
|
507
|
+
if item.devicename == devicename:
|
|
508
|
+
# add the vector to the tab
|
|
509
|
+
vector = snapshot[item.devicename][item.vectorname]
|
|
510
|
+
grpid = get_group_id(vector.group) # if grpid None, a new group has to be created
|
|
511
|
+
if grpid:
|
|
512
|
+
tabpane = self.devicesc.query_one(f"#{grpid}")
|
|
513
|
+
tabpane.add_vector(vector)
|
|
514
|
+
else:
|
|
515
|
+
#set_group_id(vector.group)
|
|
516
|
+
grouppane = self.devicesc.query_one("#dev-group-pane")
|
|
517
|
+
grouppane.add_group(vector.group)
|
|
518
|
+
|
|
519
|
+
return
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
if (self.devicesc is None) or self.startsc.is_active:
|
|
523
|
+
# no devicesc shown so return
|
|
524
|
+
return
|
|
525
|
+
|
|
526
|
+
if item.devicename != devicename:
|
|
527
|
+
# This device is not currently being shown
|
|
528
|
+
# no need to update any widgets
|
|
529
|
+
return
|
|
530
|
+
|
|
531
|
+
# so device which is currently on self.devicesc has been updated
|
|
532
|
+
# update devicesc
|
|
533
|
+
|
|
534
|
+
# device messages
|
|
535
|
+
if item.eventtype == "Message" and (not item.vectorname):
|
|
536
|
+
messages = snapshot[devicename].messages
|
|
537
|
+
if messages:
|
|
538
|
+
log = self.devicesc.query_one("#device-messages")
|
|
539
|
+
log.clear()
|
|
540
|
+
mlist = reversed([ localtimestring(t) + " " + m for t,m in messages ])
|
|
541
|
+
log.write_lines(mlist)
|
|
542
|
+
|
|
543
|
+
if not item.vectorname:
|
|
544
|
+
return
|
|
545
|
+
|
|
546
|
+
vector = snapshot[item.devicename][item.vectorname]
|
|
547
|
+
|
|
548
|
+
# display the vector timestamp and state
|
|
549
|
+
vectorpane = self.devicesc.query_one(f"#{get_id(devicename, item.vectorname)}")
|
|
550
|
+
vectorpane.vtime = localtimestring(vector.timestamp)
|
|
551
|
+
|
|
552
|
+
if item.eventtype == "TimeOut":
|
|
553
|
+
buttonstatus = self.devicesc.query_one(f"#{get_id(devicename, item.vectorname)}_submitmessage")
|
|
554
|
+
buttonstatus.update("A Timeout Error has occurred")
|
|
555
|
+
vectorpane.vstate = "Alert"
|
|
556
|
+
return
|
|
557
|
+
|
|
558
|
+
vectorpane.vstate = vector.state
|
|
559
|
+
|
|
560
|
+
if item.eventtype == "State":
|
|
561
|
+
# Only the state has changed, and that's dealt with
|
|
562
|
+
return
|
|
563
|
+
|
|
564
|
+
# Display vector message
|
|
565
|
+
if vector.message:
|
|
566
|
+
vectorpane.vmessage = localtimestring(vector.message_timestamp) + " " + vector.message
|
|
567
|
+
|
|
568
|
+
# For every member in the vector, display its value
|
|
569
|
+
|
|
570
|
+
for membername, membervalue in vector.items():
|
|
571
|
+
memberpane = self.devicesc.query_one(f"#{get_id(devicename, vector.name, membername)}")
|
|
572
|
+
if vector.vectortype == "NumberVector":
|
|
573
|
+
membervalue = vector.getformattedvalue(membername)
|
|
574
|
+
if vector.vectortype == "TextVector":
|
|
575
|
+
if not membervalue:
|
|
576
|
+
memberpane.clear_text_value()
|
|
577
|
+
if vector.vectortype == "BLOBVector":
|
|
578
|
+
memberpane.mvalue = vector.member(membername).filename
|
|
579
|
+
else:
|
|
580
|
+
memberpane.mvalue = membervalue
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def connect(self):
|
|
584
|
+
host,port = self.hostport.split(":")
|
|
585
|
+
self.make_connection(host, port, self.blobfolderpath)
|
|
586
|
+
|
|
587
|
+
def disconnect(self):
|
|
588
|
+
global _ITEMID
|
|
589
|
+
connection = get_connection()
|
|
590
|
+
if connection.is_alive():
|
|
591
|
+
connection.txque.put(None)
|
|
592
|
+
connection.clientthread.join()
|
|
593
|
+
self.queclient = None
|
|
594
|
+
self.clientthread = None
|
|
595
|
+
self.snapshot = None
|
|
596
|
+
self.host = None
|
|
597
|
+
self.port = None
|
|
598
|
+
log = self.startsc.query_one("#system-messages")
|
|
599
|
+
log.clear()
|
|
600
|
+
t = datetime.now(tz=timezone.utc)
|
|
601
|
+
log.write(localtimestring(t) + " " + "DISCONNECTED")
|
|
602
|
+
self.clear_devices()
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def clear_devices(self):
|
|
606
|
+
device_pane = self.startsc.query_one("#device-pane")
|
|
607
|
+
if device_pane.query(".devices"):
|
|
608
|
+
device_pane.remove_children(".devices")
|
|
609
|
+
device_pane.mount(Static("No Devices found", id="no-devices"))
|
|
610
|
+
_ITEMID.clear()
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def make_connection(self, host, port, blobfolder=None):
|
|
615
|
+
if (self.clientthread is not None) and self.clientthread.is_alive():
|
|
616
|
+
raise RuntimeError("Connection has to terminate before another one can be added")
|
|
617
|
+
# The calling app sets the snapshot here
|
|
618
|
+
self.snapshot = None
|
|
619
|
+
self.queclient = None
|
|
620
|
+
self.clientthread = None
|
|
621
|
+
self.host = host
|
|
622
|
+
self.port = port
|
|
623
|
+
|
|
624
|
+
if blobfolder:
|
|
625
|
+
# if no blobfolder given, self.blobfolderpath remains unchanged
|
|
626
|
+
if isinstance(blobfolder, pathlib.Path):
|
|
627
|
+
self.blobfolderpath = blobfolder
|
|
628
|
+
else:
|
|
629
|
+
self.blobfolderpath = pathlib.Path(blobfolder).expanduser().resolve()
|
|
630
|
+
if not self.blobfolderpath.is_dir():
|
|
631
|
+
raise KeyError("If given, the BLOB's folder should be an existing directory")
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
if self.host and self.port:
|
|
635
|
+
# empty queues
|
|
636
|
+
while not self.txque.empty():
|
|
637
|
+
try:
|
|
638
|
+
item = self.txque.get_nowait()
|
|
639
|
+
except queue.Empty:
|
|
640
|
+
break
|
|
641
|
+
while not self.rxque.empty():
|
|
642
|
+
try:
|
|
643
|
+
item = self.rxque.get_nowait()
|
|
644
|
+
except queue.Empty:
|
|
645
|
+
break
|
|
646
|
+
# create a QueClient object
|
|
647
|
+
self.queclient = QueClient(self.txque, self.rxque, self.host, self.port, self.blobfolderpath)
|
|
648
|
+
# create a thread to run self.queclient.asyncrun()
|
|
649
|
+
self.clientthread = threading.Thread(target=asyncio.run, args=(self.queclient.asyncrun(),))
|
|
650
|
+
self.clientthread.start()
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def is_alive(self):
|
|
654
|
+
if self.clientthread is None:
|
|
655
|
+
return False
|
|
656
|
+
if self.clientthread.is_alive():
|
|
657
|
+
return True
|
|
658
|
+
self.queclient = None
|
|
659
|
+
self.clientthread = None
|
|
660
|
+
self.snapshot = None
|