xaal.tools 0.4__tar.gz

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.
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.1
2
+ Name: xaal.tools
3
+ Version: 0.4
4
+ Summary: xAAL devices tools
5
+ Author-email: Jerome Kerdreux <Jerome.Kerdreux@imt-atlantique.fr>
6
+ License: GPL License
7
+ Keywords: xaal,tools
8
+ Classifier: Programming Language :: Python
9
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
10
+ Description-Content-Type: text/x-rst
11
+ Requires-Dist: xaal.lib
12
+ Requires-Dist: colored==1.4.3
File without changes
@@ -0,0 +1,29 @@
1
+ [project]
2
+ name = "xaal.tools"
3
+ version = "0.4"
4
+ description = "xAAL devices tools"
5
+ readme = "README.rst"
6
+ authors = [ { name = "Jerome Kerdreux", email = "Jerome.Kerdreux@imt-atlantique.fr" } ]
7
+ license = { text = "GPL License"}
8
+ classifiers = ["Programming Language :: Python",
9
+ "Topic :: Software Development :: Libraries :: Python Modules"]
10
+ keywords = ["xaal", "tools"]
11
+ dependencies = ["xaal.lib", "colored==1.4.3"]
12
+
13
+ [project.scripts]
14
+ xaal-isalive = "xaal.tools.toolbox:is_alive"
15
+ xaal-info = "xaal.tools.toolbox:info"
16
+ xaal-walker = "xaal.tools.toolbox:walker"
17
+ xaal-dumper = "xaal.tools.toolbox:dumper"
18
+ xaal-log = "xaal.tools.toolbox:log"
19
+ xaal-querydb = "xaal.tools.toolbox:query_db"
20
+ xaal-cleandb = "xaal.tools.toolbox:clean_db"
21
+ xaal-send = "xaal.tools.toolbox:send"
22
+ xaal-tail = "xaal.tools.toolbox:tail"
23
+ xaal-pkgrun = "xaal.tools.toolbox:pkgrun"
24
+ xaal-keygen = "xaal.tools.keygen:main"
25
+ xaal-uuidgen = "xaal.tools.uuidgen:main"
26
+ xaal-shell = "xaal.tools.toolbox:shell"
27
+
28
+ [tool.setuptools.packages.find]
29
+ include = ["xaal.tools"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,17 @@
1
+ """ Tool to build a key pass for xAAL config file"""
2
+
3
+ from xaal.lib import tools
4
+ import binascii
5
+
6
+ def main():
7
+ try:
8
+ temp = input("Please enter your passphrase: ")
9
+ key = tools.pass2key(temp)
10
+ print("Cut & Paste this key in your xAAL config-file")
11
+ print("key=%s"% binascii.hexlify(key).decode('utf-8'))
12
+ except KeyboardInterrupt:
13
+ print("Bye Bye..")
14
+
15
+
16
+ if __name__ == '__main__':
17
+ main()
@@ -0,0 +1,1004 @@
1
+ """
2
+ This module provides a set of utilities to interact with the xAAL bus and devices.
3
+ The module use a lot of AsyncEngine features, so the code can be a bit tricky to read.
4
+ If you're looking for simples examples, please check the legacy tools instead.
5
+ """
6
+
7
+ import sys
8
+
9
+ if sys.argv[0].endswith('pkgrun'):
10
+ # right now, some packages depend on gevent, so we need to import it here.
11
+ # this is only needed for the pkgrun command
12
+ try:
13
+ from gevent import monkey
14
+ monkey.patch_all(thread=False)
15
+ # print("Loaded gevent")
16
+ except ModuleNotFoundError:
17
+ pass
18
+
19
+ # xAAL import
20
+ from xaal.lib import AsyncEngine, Device, tools, helpers, config
21
+ from xaal.lib.messages import MessageType
22
+
23
+ # General python import
24
+ import asyncio
25
+ import time
26
+ import importlib
27
+ import logging
28
+ import enum
29
+ import optparse
30
+
31
+ # colors & styles
32
+ from colored import fore, style
33
+ from tabulate import tabulate
34
+ import pprint
35
+ import shutil # needed by the tail command
36
+
37
+
38
+ HIDE_ACTION = ['get_attributes', 'get_description', 'get_keys_values', 'get_devices', 'is_alive']
39
+ TABLE_STYLE = 'psql'
40
+ LINE = "="*78
41
+ DB_DEV_TYPE = "metadatadb.basic"
42
+
43
+
44
+ class Colors(enum.Enum):
45
+ DEFAULT = fore.WHITE
46
+ # ALIVE = fore.LIGHT_GRAY
47
+ # ATTRIBUTS = fore.LIGHT_YELLOW
48
+ # REQUEST = fore.LIGHT_RED
49
+ # IS_ALIVE = fore.LIGHT_MAGENTA
50
+ # REPLY = fore.LIGHT_CYAN
51
+ # NOTIFY = fore.LIGHT_GREEN
52
+ # DEV_TYPE = fore.LIGHT_BLUE
53
+ # ADDR = fore.LIGHT_RED
54
+ # INFO = fore.CYAN
55
+ # DB = fore.SPRING_GREEN_1
56
+
57
+ ALIVE = fore.LIGHT_GRAY
58
+ ATTRIBUTS = fore.YELLOW
59
+ REQUEST = fore.RED
60
+ IS_ALIVE = fore.MAGENTA
61
+ REPLY = fore.CYAN
62
+ NOTIFY = fore.LIGHT_GREEN
63
+ DEV_TYPE = fore.BLUE
64
+ ADDR = fore.RED
65
+ INFO = fore.CYAN
66
+ DB = fore.SPRING_GREEN_1
67
+
68
+ def __str__(self):
69
+ return self.value
70
+
71
+
72
+ class DeviceInfo(object):
73
+ def __init__(self):
74
+ self.alive = False
75
+ self.address = None
76
+ self.dev_type = None
77
+ self.description = None
78
+ self.attributes = None
79
+ self.db = None
80
+ self.ready_event = asyncio.Event()
81
+ self.displayed = False
82
+
83
+ def ready(self):
84
+ if self.address is None:
85
+ return False
86
+ if self.dev_type is None:
87
+ return False
88
+ if self.description is None:
89
+ return False
90
+ if self.attributes is None:
91
+ return False
92
+ return True
93
+
94
+ def display(self, color=True):
95
+ if color:
96
+ self.color_display()
97
+ else:
98
+ self.normal_display()
99
+
100
+ def color_display(self):
101
+ # info & description
102
+ r = []
103
+ r.append(['Informations', ''])
104
+ r.append(['============', ''])
105
+ r.append(["alive", colorize(Colors.IS_ALIVE, self.alive)])
106
+ r.append(['dev_type', colorize(Colors.DEV_TYPE, self.dev_type)])
107
+ r.append(['address', colorize(Colors.ADDR, self.address)])
108
+ if self.description and len(self.description) > 0:
109
+ for k, v in self.description.items():
110
+ if k == 'info':
111
+ v = colorize(Colors.INFO, v)
112
+ else:
113
+ v = colorize(Colors.DEFAULT, v)
114
+ r.append([k, v])
115
+
116
+ # attributes
117
+ if self.attributes and len(self.attributes) > 0:
118
+ # tabulate has no minimal width so used this trick
119
+ r.append(['-'*22, '-'*46])
120
+ r.append(['Attributes', ''])
121
+ r.append(['==========', ''])
122
+ for k, v in self.attributes.items():
123
+ v = pprint.pformat(v, width=55).split('\n')
124
+ tmp = ''
125
+ for line in v:
126
+ tmp = tmp + colorize(Colors.ATTRIBUTS, line) + '\n'
127
+ r.append([k, tmp])
128
+
129
+ # metadata
130
+ if self.db and len(self.db.keys()) > 0:
131
+ r.append(['-'*22, '-'*46])
132
+ r.append(['Metadata', ''])
133
+ r.append(['========', ''])
134
+ for k, v in self.db.items():
135
+ v = colorize(Colors.DB, v)
136
+ r.append([k, v])
137
+ print(tabulate(r, tablefmt=TABLE_STYLE))
138
+
139
+ def normal_display(self):
140
+ # info & description
141
+ r = []
142
+ r.append(['Informations', ''])
143
+ r.append(['============', ''])
144
+ r.append(["alive", self.alive])
145
+ r.append(['dev_type', self.dev_type])
146
+ r.append(['address', self.address])
147
+ for k, v in self.description.items():
148
+ r.append([k, v])
149
+
150
+ # attributes
151
+ if len(self.attributes) > 0:
152
+ # tabulate has no minimal width so used this trick
153
+ r.append(['-'*22, '-'*46])
154
+ r.append(['Attributes', ''])
155
+ r.append(['==========', ''])
156
+ for k, v in self.attributes.items():
157
+ v = pprint.pformat(v, width=55).split('\n')
158
+ tmp = ''
159
+ for line in v:
160
+ tmp = tmp + line + '\n'
161
+ r.append([k, tmp])
162
+ # metadata
163
+ if self.db and len(self.db.keys()) > 0:
164
+ r.append(['-'*22, '-'*46])
165
+ r.append(['Metadata', ''])
166
+ r.append(['========', ''])
167
+ for k, v in self.db.items():
168
+ r.append([k, v])
169
+ print(tabulate(r, tablefmt=TABLE_STYLE))
170
+
171
+
172
+ class ToolboxHelper(object):
173
+ def __init__(self) -> None:
174
+ self.name = None # cmdline name
175
+ self.parser = None
176
+ self.engine = None # toolbox engine
177
+ self.device = None # current device
178
+ self.devices = [] # devices list (alive / walker)
179
+ # idle detector / force exit
180
+ self.exit_event = asyncio.Event()
181
+ self.last_msg_time = now()
182
+
183
+ # display time
184
+ self.start_time = 0
185
+
186
+ # db server
187
+ self.db_server = None
188
+ self.db_server_found = asyncio.Event()
189
+
190
+ # Let's start
191
+ self.setup_name()
192
+ self.setup_parser()
193
+
194
+ def setup_name(self):
195
+ name = sys.argv[0].split('/')[-1]
196
+ # helpers.set_console_title(name)
197
+ self.name = name
198
+ return self.name
199
+
200
+ def setup_parser(self, usage=None):
201
+ self.parser = optparse.OptionParser(usage=usage)
202
+ self.parser.add_option("-c", dest="no_color", help="disable color", action="store_true", default=False)
203
+ self.parser.add_option("-l", dest="debug", help="Enable logging", action="store_true", default=False)
204
+ self.parser.add_option("-a", dest="mcast_addr", help="Multicast address", default=config.address)
205
+ self.parser.add_option("-p", dest="mcast_port", help="Multicast port", default=config.port)
206
+ return self.parser
207
+
208
+ def setup_device(self):
209
+ # toolbox device
210
+ dev = Device("cli.basic")
211
+ dev.address = tools.get_random_uuid()
212
+ dev.info = f'Aiotoolbox CLI {self.name}'
213
+ self.device = dev
214
+ self.engine.add_device(self.device)
215
+ return self.device
216
+
217
+ def setup_engine(self):
218
+ # engine
219
+ bus_addr = self.options.mcast_addr
220
+ try:
221
+ bus_port = int(self.options.mcast_port)
222
+ except ValueError:
223
+ self.error("Invalid port number")
224
+ eng = AsyncEngine(address=bus_addr, port=bus_port)
225
+ eng.disable_msg_filter()
226
+ # start the engine
227
+ self.engine = eng
228
+ self.start_time = now()
229
+ eng.start()
230
+ return eng
231
+
232
+ def setup_basic(self):
233
+ eng = self.setup_engine()
234
+ dev = self.setup_device()
235
+ return (eng, dev)
236
+
237
+ #####################################################
238
+ # command line parsing
239
+ #####################################################
240
+ def setup_msg_parser(self):
241
+ self.engine.subscribe(self.parse_msg)
242
+
243
+ def parse(self):
244
+ self.options, self.args = self.parser.parse_args()
245
+ if self.options.debug:
246
+ helpers.setup_console_logger()
247
+ return self.options, self.args
248
+
249
+ def check_address(self, value):
250
+ addr = None
251
+ if value:
252
+ addr = tools.get_uuid(value)
253
+ if addr is None:
254
+ self.error(f"Invalid address: {value}")
255
+ return addr
256
+
257
+ def check_devtype(self, value):
258
+ dev_type = 'any.any'
259
+ if value:
260
+ if not tools.is_valid_dev_type(value):
261
+ self.error("Invalid device type: %s" % value)
262
+ dev_type = value
263
+ return dev_type
264
+
265
+ #####################################################
266
+ # devices
267
+ #####################################################
268
+ def add_device(self, dev):
269
+ if dev.address:
270
+ if self.get_device(dev.address):
271
+ # device already know
272
+ return
273
+ self.devices.append(dev)
274
+
275
+ def get_device(self, addr):
276
+ for k in self.devices:
277
+ if k.address == addr:
278
+ return k
279
+ return None
280
+
281
+ def new_device(self, addr):
282
+ dev = DeviceInfo()
283
+ dev.address = addr
284
+ self.add_device(dev)
285
+ return dev
286
+
287
+ def display_device(self, dev):
288
+ # show device only once
289
+ if dev.displayed:
290
+ return
291
+
292
+ if self.options.no_color:
293
+ dev.normal_display()
294
+ else:
295
+ dev.color_display()
296
+ dev.displayed = True
297
+
298
+ def is_ready(self, dev):
299
+ if self.db_server:
300
+ if dev.db is None:
301
+ return False
302
+ return dev.ready()
303
+
304
+ #####################################################
305
+ # messages
306
+ #####################################################
307
+ def dump_msg(self, msg):
308
+ color = not self.options.no_color
309
+ color_value = self.color_for_msg(msg)
310
+ if color:
311
+ print(color_value, end='')
312
+ msg.dump()
313
+ if color:
314
+ print(style.RESET, end='')
315
+
316
+ def color_for_msg(self, msg):
317
+ color_value = Colors.DEFAULT
318
+ if msg.is_request_isalive():
319
+ color_value = Colors.IS_ALIVE
320
+ elif msg.is_alive():
321
+ color_value = Colors.ALIVE
322
+ elif msg.is_attributes_change():
323
+ color_value = Colors.ATTRIBUTS
324
+ elif msg.is_request():
325
+ color_value = Colors.REQUEST
326
+ elif msg.is_reply():
327
+ color_value = Colors.REPLY
328
+ elif msg.is_notify():
329
+ color_value = Colors.NOTIFY
330
+ return color_value
331
+
332
+ def parse_msg(self, msg):
333
+ """ default parser used for info/walker"""
334
+ target = self.get_device(msg.source)
335
+ if target is None:
336
+ target = DeviceInfo()
337
+ target.address = msg.source
338
+ target.dev_type = msg.dev_type
339
+ self.add_device(target)
340
+ if target.dev_type is None:
341
+ target.dev_type = msg.dev_type
342
+ if msg.is_get_attribute_reply():
343
+ target.attributes = msg.body
344
+ elif msg.is_get_description_reply():
345
+ target.description = msg.body
346
+ elif msg.is_alive():
347
+ target.alive = True
348
+ return target
349
+
350
+ def parse_db_msg(self, msg):
351
+ found = msg.body.get('device', None)
352
+ found_map = msg.body.get('map', None)
353
+ tmp = self.get_device(found)
354
+ if not tmp:
355
+ tmp = self.new_device(found)
356
+ tmp.db = found_map
357
+ return tmp
358
+
359
+ def request_info(self, addr):
360
+ self.engine.send_get_description(self.device, [addr])
361
+ self.engine.send_get_attributes(self.device, [addr])
362
+
363
+ def request_is_alive(self, addr=None, dev_type=None):
364
+ if addr:
365
+ self.engine.send_is_alive(self.device, [addr])
366
+ elif dev_type:
367
+ self.engine.send_is_alive(self.device, dev_types=[dev_type])
368
+ else:
369
+ self.engine.send_is_alive(self.device)
370
+
371
+ def request_action(self, addr, action, body=None):
372
+ self.engine.send_request(self.device, [addr], action, body)
373
+
374
+ #####################################################
375
+ # db server
376
+ #####################################################
377
+ def find_db_callback(self, msg):
378
+ if not match_dev_type(msg, DB_DEV_TYPE):
379
+ return
380
+ # new db server found
381
+ if msg.is_alive():
382
+ self.db_server = msg.source
383
+ self.db_server_found.set()
384
+
385
+ async def find_db_server(self):
386
+ self.engine.subscribe(self.find_db_callback)
387
+ self.engine.send_is_alive(self.device, dev_types=[DB_DEV_TYPE])
388
+ await wait_for_event(self.db_server_found, timeout=0.3)
389
+ self.engine.unsubscribe(self.find_db_callback)
390
+
391
+ def request_db_values(self, addr):
392
+ if self.db_server:
393
+ self.engine.send_request(self.device, [self.db_server, ], "get_keys_values", {'device': addr})
394
+
395
+ def request_db_devices(self, key, value):
396
+ if self.db_server:
397
+ self.engine.send_request(self.device, [self.db_server, ], "get_devices", {'key': key, 'value': value})
398
+
399
+ def is_db_reply(self, msg):
400
+ if match_dev_type(msg, DB_DEV_TYPE) and msg.is_reply() and self.device.address in msg.targets:
401
+ return True
402
+ return False
403
+
404
+ #####################################################
405
+ # start/stop/idle/error
406
+ #####################################################
407
+ async def wait_completed(self, timeout=0.5):
408
+ """wait until exit event is set"""
409
+ await wait_for_event(self.exit_event, timeout=timeout)
410
+
411
+ def update_idle(self):
412
+ self.last_msg_time = now()
413
+
414
+ async def idle_detector(self):
415
+ while True:
416
+ await asyncio.sleep(0.1)
417
+ if now() - self.last_msg_time > 0.4:
418
+ break
419
+ self.quit()
420
+
421
+ def run_until_timeout(self, timeout=3):
422
+ """ run the engine until timeout """
423
+ self.engine.add_timer(self.quit, timeout)
424
+ self.engine.run()
425
+
426
+ def run_until_idle(self):
427
+ self.engine.new_task(self.idle_detector())
428
+ self.engine.run()
429
+
430
+ def run_forever(self):
431
+ self.engine.run()
432
+
433
+ def quit(self):
434
+ self.engine.shutdown()
435
+ print()
436
+ print(LINE)
437
+ print(f"Found devices: {len(self.devices)}")
438
+ if self.db_server:
439
+ print(f"Metadb server: {self.db_server}")
440
+ t = round(now() - self.start_time, 2)
441
+ print(f"Total runtime: {t}s")
442
+ print(LINE)
443
+
444
+ def error(self, error_msg):
445
+ print(f"error: {error_msg}")
446
+ self.parser.print_help()
447
+ exit(1)
448
+
449
+
450
+ def colorize(color, text):
451
+ return f"{color}{text}{style.RESET}"
452
+
453
+
454
+ def now():
455
+ return time.time()
456
+
457
+
458
+ def match_dev_type(msg, dev_type):
459
+ if dev_type == 'any.any':
460
+ return True
461
+ if dev_type.endswith('.any'):
462
+ subtype = msg.dev_type.split('.')[0] + '.any'
463
+ if subtype == dev_type:
464
+ return True
465
+ else:
466
+ if msg.dev_type == dev_type:
467
+ return True
468
+ return False
469
+
470
+
471
+ def match_address(msg, addr):
472
+ if (msg.source == addr) or (addr in msg.targets):
473
+ return True
474
+ return False
475
+
476
+
477
+ async def wait_for_event(event, timeout):
478
+ """ wait for a given event or timeout"""
479
+ wait_task = asyncio.create_task(event.wait())
480
+ await asyncio.wait([wait_task], timeout=timeout)
481
+
482
+
483
+ #####################################################
484
+ # dumper
485
+ #####################################################
486
+ def dumper():
487
+ helper = ToolboxHelper()
488
+ helper.parser.add_option("-f", dest="filter_address", help="only show given address")
489
+ helper.parser.add_option("-t", dest="filter_type", help="only show given device type")
490
+
491
+ helper.parse()
492
+ target = helper.check_address(helper.options.filter_address)
493
+ dev_type = helper.check_devtype(helper.options.filter_type)
494
+
495
+ eng = helper.setup_engine()
496
+
497
+ async def dumper_callback(msg):
498
+ # filter by address or dev_type
499
+ if target and not match_address(msg, target):
500
+ return
501
+ if dev_type and not match_dev_type(msg, dev_type):
502
+ return
503
+
504
+ # dump message
505
+ helper.dump_msg(msg)
506
+
507
+ eng.subscribe(dumper_callback)
508
+ helper.run_forever()
509
+
510
+ #####################################################
511
+ # alive
512
+ #####################################################
513
+ def is_alive():
514
+ helper = ToolboxHelper()
515
+ helper.parser.add_option("-t", dest="filter_type", help="only show given device type")
516
+ helper.parse()
517
+ dev_type = helper.check_devtype(helper.options.filter_type)
518
+ color = not helper.options.no_color
519
+
520
+ (eng, dev) = helper.setup_basic()
521
+
522
+ async def alive_callback(msg):
523
+ if (msg.source == dev.address) or (msg.is_alive() is False):
524
+ return
525
+ if match_dev_type(msg, dev_type) is False:
526
+ return
527
+
528
+ # idle detectiong
529
+ helper.update_idle()
530
+ if helper.get_device(msg.source) is None:
531
+ helper.parse_msg(msg)
532
+ if color:
533
+ print(f"{colorize(Colors.ADDR,msg.source)}: {colorize(Colors.DEV_TYPE,msg.dev_type)}")
534
+ else:
535
+ print(f"{msg.source}: {msg.dev_type}")
536
+
537
+ def start():
538
+ print(LINE)
539
+ helper.request_is_alive(dev_type=dev_type)
540
+
541
+ eng.subscribe(alive_callback)
542
+ eng.on_start(start)
543
+ helper.run_until_idle()
544
+
545
+ #####################################################
546
+ # info
547
+ #####################################################
548
+ def info():
549
+ helper = ToolboxHelper()
550
+ # redefine parser to add usage
551
+ helper.setup_parser("Usage: %prog [options] device_address")
552
+ (_, args) = helper.parse()
553
+ # command line address
554
+ if len(args) != 1:
555
+ helper.error("empty address")
556
+ target = tools.get_uuid(args[0])
557
+ if target is None:
558
+ helper.error("Invalid address: %s" % args[0])
559
+
560
+ (eng, dev) = helper.setup_basic()
561
+
562
+ def ready_to_show(dev):
563
+ if dev and helper.is_ready(dev):
564
+ helper.display_device(dev)
565
+ helper.exit_event.set()
566
+
567
+ def info_callback(msg, addr=target):
568
+ # collecting description and attributes
569
+ if msg.source != addr:
570
+ return
571
+ found = helper.parse_msg(msg)
572
+ ready_to_show(found)
573
+
574
+ def query_db_callback(msg):
575
+ if helper.is_db_reply(msg):
576
+ found = helper.parse_db_msg(msg)
577
+ ready_to_show(found)
578
+
579
+ async def run():
580
+ await helper.find_db_server()
581
+ if helper.db_server:
582
+ helper.request_db_values(target)
583
+ eng.subscribe(query_db_callback)
584
+ helper.request_is_alive(addr=target)
585
+ helper.request_info(target)
586
+ await helper.wait_completed()
587
+ helper.quit()
588
+
589
+ eng.subscribe(info_callback)
590
+ eng.on_start(run)
591
+ helper.run_forever()
592
+
593
+ #####################################################
594
+ # walker
595
+ #####################################################
596
+ def walker():
597
+ helper = ToolboxHelper()
598
+ helper.parser.add_option("-t", dest="filter_type", help="only show given device type")
599
+ helper.parse()
600
+ dev_type = helper.check_devtype(helper.options.filter_type)
601
+ (eng, dev) = helper.setup_basic()
602
+
603
+ def ready_to_show(dev):
604
+ if dev and helper.is_ready(dev):
605
+ helper.display_device(dev)
606
+
607
+ async def walker_callback(msg):
608
+ if msg.source == dev.address:
609
+ return
610
+ if match_dev_type(msg, DB_DEV_TYPE):
611
+ return
612
+ if match_dev_type(msg, dev_type) is False:
613
+ return
614
+
615
+ found = helper.parse_msg(msg)
616
+ helper.update_idle()
617
+ if msg.is_alive():
618
+ if not found.ready():
619
+ helper.request_info(msg.source)
620
+ helper.request_db_values(msg.source)
621
+ ready_to_show(found)
622
+
623
+ async def query_db_callback(msg):
624
+ if helper.is_db_reply(msg):
625
+ helper.update_idle()
626
+ found = helper.parse_db_msg(msg)
627
+ ready_to_show(found)
628
+
629
+ async def start():
630
+ await helper.find_db_server()
631
+ helper.update_idle()
632
+ if helper.db_server:
633
+ eng.subscribe(query_db_callback)
634
+ eng.subscribe(walker_callback)
635
+ helper.request_is_alive(dev_type=dev_type)
636
+
637
+ eng.on_start(start)
638
+ helper.run_until_idle()
639
+
640
+ #####################################################
641
+ # log
642
+ #####################################################
643
+ def log():
644
+ helper = ToolboxHelper()
645
+ helper.parser.add_option("-f", dest="filter_address", help="only show given address")
646
+ helper.parser.add_option("-t", dest="filter_type", help="only show given device type")
647
+ helper.parse()
648
+
649
+ target = helper.check_address(helper.options.filter_address)
650
+ dev_type = helper.check_devtype(helper.options.filter_type)
651
+ color = not helper.options.no_color
652
+
653
+ eng = helper.setup_engine()
654
+
655
+ def log_callback(msg):
656
+ if msg.is_alive() or (msg.action in HIDE_ACTION):
657
+ return
658
+ if target and not match_address(msg, target):
659
+ return
660
+ if dev_type and not match_dev_type(msg, dev_type):
661
+ return
662
+
663
+ color_value = Colors.DEFAULT
664
+ if msg.is_attributes_change():
665
+ color_value = Colors.ATTRIBUTS
666
+ elif msg.is_notify():
667
+ color_value = Colors.NOTIFY
668
+ elif msg.is_request():
669
+ color_value = Colors.REQUEST
670
+ elif msg.is_reply():
671
+ color_value = Colors.REPLY
672
+
673
+ if color:
674
+ dump = f"{Colors.DEFAULT}{time.ctime()} {Colors.ADDR}{msg.source} {Colors.DEV_TYPE}{msg.dev_type}\t{color_value}{msg.action} {msg.body}{style.RESET}"
675
+ else:
676
+ dump = f"{time.ctime()} {msg.source} {msg.dev_type}\t{msg.action} {msg.body}"
677
+ print(dump)
678
+
679
+ eng.subscribe(log_callback)
680
+ helper.run_forever()
681
+
682
+ #####################################################
683
+ # query db
684
+ #####################################################
685
+ def query_db():
686
+ helper = ToolboxHelper()
687
+ helper.parser.add_option("-d", dest="device_address", help="search by device address")
688
+ helper.parser.add_option("-k", dest="key", help="search by key")
689
+ helper.parser.add_option("-v", dest="value", help="search by value")
690
+ helper.parse()
691
+ # command line parsing
692
+ target = helper.check_address(helper.options.device_address)
693
+ key = helper.options.key
694
+ value = helper.options.value
695
+ color = not helper.options.no_color
696
+
697
+ if target and key:
698
+ helper.error("-d and -k are exclusive")
699
+
700
+ if not (target or key):
701
+ helper.error("-d or -k is required")
702
+
703
+ if value and not key:
704
+ helper.error("-v requires -k")
705
+
706
+ (eng, dev) = helper.setup_basic()
707
+
708
+ def device_callback(msg):
709
+ # search by device address
710
+ if helper.is_db_reply(msg):
711
+ found = msg.body.get('device', None)
712
+ found_map = msg.body.get('map', None)
713
+ if found == target:
714
+ r = []
715
+ r.append(['Metadata', ''])
716
+ r.append(['========', ''])
717
+ if color:
718
+ r.append(['Server:', colorize(Colors.ADDR, helper.db_server)])
719
+ else:
720
+ r.append(['Server:', helper.db_server])
721
+ for k, v in found_map.items():
722
+ if color:
723
+ v = colorize(Colors.DB, v)
724
+ r.append([k, v])
725
+ print(tabulate(r, tablefmt=TABLE_STYLE))
726
+ helper.exit_event.set()
727
+
728
+ def key_callback(msg):
729
+ # search by key / value
730
+ if helper.is_db_reply(msg):
731
+ k = msg.body.get('key', None)
732
+ v = msg.body.get('value', None)
733
+ print(LINE)
734
+ print(f"Search result for key={k} value={v}")
735
+ print(LINE)
736
+ devs = msg.body.get('devices', [])
737
+ for dev in devs:
738
+ if color:
739
+ print(f"- {colorize(Colors.ADDR,dev)}")
740
+ else:
741
+ print(f"- {dev}")
742
+ print(f"\n# Found {len(devs)} devices")
743
+ helper.exit_event.set()
744
+
745
+ async def run():
746
+ await helper.find_db_server()
747
+ if helper.db_server:
748
+ # found db server, send request and wait to complete
749
+ if target:
750
+ eng.subscribe(device_callback)
751
+ helper.request_db_values(target)
752
+ await helper.wait_completed()
753
+ elif key:
754
+ eng.subscribe(key_callback)
755
+ helper.request_db_devices(key, value)
756
+ await helper.wait_completed()
757
+ else:
758
+ print("\nNo metadata server found")
759
+ helper.quit()
760
+
761
+ eng.on_start(run)
762
+ helper.run_forever()
763
+
764
+ #####################################################
765
+ # cleanup db
766
+ #####################################################
767
+ def clean_db():
768
+ helper = ToolboxHelper()
769
+ helper.parse()
770
+
771
+ (eng, dev) = helper.setup_basic()
772
+ db_devices = []
773
+
774
+ def alive_callback(msg):
775
+ if (msg.source == dev.address) or (msg.is_alive() is False):
776
+ return
777
+ if helper.get_device(msg.source) is None:
778
+ helper.parse_msg(msg)
779
+ # print(msg)
780
+
781
+ def devices_callback(msg):
782
+ if helper.is_db_reply(msg) and msg.action == 'get_devices':
783
+ r = msg.body.get('devices', [])
784
+ for k in r:
785
+ db_devices.append(k)
786
+
787
+ async def gather():
788
+ print("Gathering devices infos...")
789
+ await asyncio.sleep(2)
790
+ print("Done..")
791
+ missing = []
792
+ for k in db_devices:
793
+ dev = helper.get_device(tools.get_uuid(k))
794
+ if not dev:
795
+ missing.append(k)
796
+ for k in missing:
797
+ drop = input(f"Drop {k} from db server ? [Y|*]")
798
+ if drop == 'Y':
799
+ body = {"device": k, "map": None}
800
+ helper.request_action(helper.db_server, "update_keys_values", body)
801
+ await asyncio.sleep(0)
802
+
803
+ async def start():
804
+ await helper.find_db_server()
805
+ if helper.db_server:
806
+ eng.subscribe(alive_callback)
807
+ eng.subscribe(devices_callback)
808
+ helper.request_is_alive()
809
+ helper.request_db_devices(None, None)
810
+ await gather()
811
+ helper.quit()
812
+
813
+ else:
814
+ print("\nNo metadata server found")
815
+ # helper.quit()
816
+
817
+ eng.on_start(start)
818
+ helper.run_forever()
819
+ # helper.run_until_idle()
820
+
821
+ #####################################################
822
+ # send request
823
+ #####################################################
824
+ def send():
825
+ helper = ToolboxHelper()
826
+ helper.parser.add_option("-d", dest="target_address", help="target device address")
827
+ helper.parser.add_option("-r", dest="request_action", help="request action")
828
+ helper.parser.add_option("-b", dest="request_body", help="request body example: 'smooth:10 speed:20'")
829
+ helper.parse()
830
+
831
+ # cmd line parsing
832
+ target = helper.check_address(helper.options.target_address)
833
+ action = helper.options.request_action
834
+ if not (target and action):
835
+ helper.error("-d and -r are mandatory")
836
+ # body parsing
837
+ tmp = helper.options.request_body
838
+ body = None
839
+ if tmp:
840
+ body = {}
841
+ for k in tmp.split(' '):
842
+ (i, j) = k.split(':')
843
+ body[i] = j
844
+ # let's go
845
+ (eng, dev) = helper.setup_basic()
846
+
847
+ def action_callback(msg):
848
+ if msg.is_alive():
849
+ return
850
+ if msg.source == target:
851
+ helper.dump_msg(msg)
852
+ helper.exit_event.set()
853
+
854
+ async def run():
855
+ helper.request_action(target, action, body)
856
+ # wait at least 1 msg
857
+ await helper.wait_completed(2)
858
+ helper.quit()
859
+
860
+ eng.subscribe(action_callback)
861
+ eng.on_start(run)
862
+ helper.run_forever()
863
+
864
+ #####################################################
865
+ # tail msg
866
+ #####################################################
867
+ def tail():
868
+ helper = ToolboxHelper()
869
+ helper.parser.add_option("-f", dest="filter_address", help="only show given address")
870
+ helper.parser.add_option("-t", dest="filter_type", help="only show given device type")
871
+ helper.parser.add_option("-m", dest="filter_mode", help="hide some messages:\n 1=alives, 2=core actions, 3=replies, 4=all except notif")
872
+
873
+ helper.parse()
874
+ target = helper.check_address(helper.options.filter_address)
875
+ dev_type = helper.check_devtype(helper.options.filter_type)
876
+ mode = helper.options.filter_mode
877
+ if mode:
878
+ try:
879
+ mode = int(mode)
880
+ except ValueError:
881
+ helper.error("-m must be an integer")
882
+ if mode not in range(1, 5):
883
+ helper.error("-m must be in range 1-4")
884
+ else:
885
+ mode = 0
886
+ color = not helper.options.no_color
887
+ eng = helper.setup_engine()
888
+
889
+ def tail_callback(msg):
890
+ if mode > 0 and msg.is_alive():
891
+ return
892
+ if target and not match_address(msg, target):
893
+ return
894
+ if dev_type and not match_dev_type(msg, dev_type):
895
+ return
896
+ if mode > 1 and msg.action in HIDE_ACTION:
897
+ return
898
+ if mode > 2 and msg.is_reply():
899
+ return
900
+ if mode > 3 and msg.is_request():
901
+ return
902
+
903
+ color_value = helper.color_for_msg(msg)
904
+ schem = '*'
905
+ if msg.msg_type == MessageType.REQUEST.value:
906
+ schem = '>'
907
+ elif msg.msg_type == MessageType.REPLY.value:
908
+ schem = '<'
909
+ elif msg.msg_type == MessageType.NOTIFY.value:
910
+ schem = '='
911
+
912
+ targets = [tools.reduce_addr(addr) for addr in msg.targets]
913
+ tmp = shutil.get_terminal_size()[0] - (2 + 18 + 36 + 20 + 16 + 7)
914
+ if tmp < 22:
915
+ tmp = 22
916
+ BODY_FORMAT = '%-22.'+str(tmp)+'s'
917
+ FORMAT = '%-2.02s %-18.18s %-36.36s (%-20.20s) %-16.16s '+BODY_FORMAT
918
+ res = FORMAT % (schem, msg.action, msg.source, msg.dev_type, targets, msg.body)
919
+ if color:
920
+ print(colorize(color_value, res))
921
+ else:
922
+ print(res)
923
+
924
+ # FIXME: find a better way to do this
925
+ print('\x1bc', end='') # clear screen
926
+ FORMAT = '%-2.02s %-18.18s %-36.36s (%-20.20s) %-16.16s %-22.22s'
927
+ print(FORMAT % ('=', 'action', 'source', 'dev_type', 'targets', 'body'))
928
+
929
+ eng.subscribe(tail_callback)
930
+ helper.run_forever()
931
+
932
+ #####################################################
933
+ # run pkg
934
+ #####################################################
935
+ def pkgrun():
936
+ helper = ToolboxHelper()
937
+ # redefine parser to add usage
938
+ helper.setup_parser("Usage: %prog [options] pkg1 pkg2 ...")
939
+ (_, args) = helper.parse()
940
+ eng = helper.setup_engine()
941
+ logger = logging.getLogger(helper.name)
942
+
943
+ def load_pkgs():
944
+ for k in args:
945
+ xaal_mod = 'xaal.' + k
946
+ try:
947
+ mod = importlib.import_module(xaal_mod)
948
+ except ModuleNotFoundError:
949
+ logger.critical("Unable to load module: %s" % xaal_mod)
950
+ continue
951
+
952
+ if hasattr(mod, 'setup') is False:
953
+ logger.critical("Unable to find setup %s" % xaal_mod)
954
+ continue
955
+ logger.info(f"{xaal_mod} loaded")
956
+ result = mod.setup(eng)
957
+ if result is not True:
958
+ logger.critical("something goes wrong with package: %s" % xaal_mod)
959
+
960
+ load_pkgs()
961
+ helper.run_forever()
962
+
963
+ #####################################################
964
+ # ipdb shell on steroid
965
+ #####################################################
966
+ def shell():
967
+ helper = ToolboxHelper()
968
+ helper.parse()
969
+ eng = helper.setup_engine()
970
+ eng.disable_msg_filter()
971
+
972
+ # load IPython
973
+ try:
974
+ import IPython
975
+ from IPython.lib import backgroundjobs
976
+ except ModuleNotFoundError:
977
+ print("Error: Unable to load IPython\n")
978
+ print("Please install IPython to use xAAL shell")
979
+ print("$ pip install ipython\n")
980
+ exit(1)
981
+
982
+ logging.getLogger("parso").setLevel(logging.WARNING)
983
+ logging.getLogger("blib2to3").setLevel(logging.WARNING)
984
+
985
+ # run the engine in background
986
+ jobs = backgroundjobs.BackgroundJobManager()
987
+ jobs.new(eng.run)
988
+
989
+ # imported modules for convenient use in the shell
990
+ from xaal.schemas import devices
991
+ from xaal.lib import Message, Attribute, Device
992
+ from xaal.monitor import Monitor
993
+
994
+ IPython.embed(banner1="============================== xAAL Shell ==============================",
995
+ banner2=f"* AsyncEngine running in background:\n* eng = {eng}\n\n",
996
+ colors="Linux",
997
+ confirm_exit=False,
998
+ separate_in='',
999
+ autoawait=True)
1000
+
1001
+ print("* Ending Engine")
1002
+ eng.shutdown()
1003
+ print("* Bye bye")
1004
+
@@ -0,0 +1,25 @@
1
+ from xaal.lib import tools
2
+ import sys
3
+
4
+
5
+ def main():
6
+ uuid = None
7
+ if len(sys.argv) > 1:
8
+ value = sys.argv[1]
9
+ uuid = tools.get_uuid(value)
10
+ if uuid == None:
11
+ uuid=tools.get_random_uuid()
12
+ print("TXT: %s" % uuid)
13
+
14
+ print("HEX: ",end="")
15
+ for b in uuid.bytes:
16
+ print("0x%x" % b,end=',')
17
+ print()
18
+
19
+ print("INT: ",end="")
20
+ for b in uuid.bytes:
21
+ print("%d" % b,end=',')
22
+ print()
23
+
24
+ if __name__ == '__main__':
25
+ main()
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.1
2
+ Name: xaal.tools
3
+ Version: 0.4
4
+ Summary: xAAL devices tools
5
+ Author-email: Jerome Kerdreux <Jerome.Kerdreux@imt-atlantique.fr>
6
+ License: GPL License
7
+ Keywords: xaal,tools
8
+ Classifier: Programming Language :: Python
9
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
10
+ Description-Content-Type: text/x-rst
11
+ Requires-Dist: xaal.lib
12
+ Requires-Dist: colored==1.4.3
@@ -0,0 +1,12 @@
1
+ README.rst
2
+ pyproject.toml
3
+ xaal.tools.egg-info/PKG-INFO
4
+ xaal.tools.egg-info/SOURCES.txt
5
+ xaal.tools.egg-info/dependency_links.txt
6
+ xaal.tools.egg-info/entry_points.txt
7
+ xaal.tools.egg-info/requires.txt
8
+ xaal.tools.egg-info/top_level.txt
9
+ xaal/tools/__init__.py
10
+ xaal/tools/keygen.py
11
+ xaal/tools/toolbox.py
12
+ xaal/tools/uuidgen.py
@@ -0,0 +1,14 @@
1
+ [console_scripts]
2
+ xaal-cleandb = xaal.tools.toolbox:clean_db
3
+ xaal-dumper = xaal.tools.toolbox:dumper
4
+ xaal-info = xaal.tools.toolbox:info
5
+ xaal-isalive = xaal.tools.toolbox:is_alive
6
+ xaal-keygen = xaal.tools.keygen:main
7
+ xaal-log = xaal.tools.toolbox:log
8
+ xaal-pkgrun = xaal.tools.toolbox:pkgrun
9
+ xaal-querydb = xaal.tools.toolbox:query_db
10
+ xaal-send = xaal.tools.toolbox:send
11
+ xaal-shell = xaal.tools.toolbox:shell
12
+ xaal-tail = xaal.tools.toolbox:tail
13
+ xaal-uuidgen = xaal.tools.uuidgen:main
14
+ xaal-walker = xaal.tools.toolbox:walker
@@ -0,0 +1,2 @@
1
+ xaal.lib
2
+ colored==1.4.3
@@ -0,0 +1 @@
1
+ xaal