meshcore-cli 1.1.40__py3-none-any.whl → 1.2.0__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.
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/python
2
- """
2
+ """
3
3
  mccli.py : CLI interface to MeschCore BLE companion app
4
4
  """
5
+
5
6
  import asyncio
6
7
  import os, sys
7
8
  import time, datetime
@@ -9,21 +10,27 @@ import getopt, json, shlex, re
9
10
  import logging
10
11
  import requests
11
12
  from bleak import BleakScanner, BleakClient
13
+ from bleak.exc import BleakError
12
14
  import serial.tools.list_ports
13
15
  from pathlib import Path
14
16
  import traceback
15
17
  from prompt_toolkit.shortcuts import PromptSession
16
18
  from prompt_toolkit.shortcuts import CompleteStyle
17
19
  from prompt_toolkit.completion import NestedCompleter
20
+ from prompt_toolkit.completion import CompleteEvent, Completer, Completion
18
21
  from prompt_toolkit.history import FileHistory
19
22
  from prompt_toolkit.formatted_text import ANSI
20
23
  from prompt_toolkit.key_binding import KeyBindings
21
24
  from prompt_toolkit.shortcuts import radiolist_dialog
25
+ from prompt_toolkit.completion.word_completer import WordCompleter
26
+ from prompt_toolkit.document import Document
27
+
28
+ import re
22
29
 
23
30
  from meshcore import MeshCore, EventType, logger
24
31
 
25
32
  # Version
26
- VERSION = "v1.1.40"
33
+ VERSION = "v1.2.0"
27
34
 
28
35
  # default ble address is stored in a config file
29
36
  MCCLI_CONFIG_DIR = str(Path.home()) + "/.config/meshcore/"
@@ -296,6 +303,29 @@ async def subscribe_to_msgs(mc, json_output=False, above=False):
296
303
  CS = mc.subscribe(EventType.CHANNEL_MSG_RECV, handle_message)
297
304
  await mc.start_auto_message_fetching()
298
305
 
306
+ # redefine get_completion to let user put symbols in first item
307
+ # and handle navigating in path ...
308
+ class MyNestedCompleter(NestedCompleter):
309
+ def get_completions( self, document, complete_event):
310
+ txt = document.text_before_cursor.lstrip()
311
+ if not " " in txt:
312
+ if txt != "" and txt[0] == "/" and txt.count("/") == 1:
313
+ opts = []
314
+ for k in self.options.keys():
315
+ if k[0] == "/" :
316
+ v = "/" + k.split("/")[1] #+ ("/" if k.count("/") == 2 else "")
317
+ if v not in opts:
318
+ opts.append(v)
319
+ else:
320
+ opts = self.options.keys()
321
+ completer = WordCompleter(
322
+ opts, ignore_case=self.ignore_case,
323
+ pattern=re.compile(r"([a-zA-Z0-9_\\/]+|[^a-zA-Z0-9_\s]+)"))
324
+ yield from completer.get_completions(document, complete_event)
325
+ else: # normal behavior for remainder
326
+ yield from super().get_completions(document, complete_event)
327
+
328
+
299
329
  def make_completion_dict(contacts, pending={}, to=None, channels=None):
300
330
  contact_list = {}
301
331
  pending_list = {}
@@ -306,7 +336,6 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
306
336
  if not process_event_message.last_node is None:
307
337
  to_list["!"] = None
308
338
  to_list[".."] = None
309
- to_list["public"] = None
310
339
 
311
340
  it = iter(contacts.items())
312
341
  for c in it :
@@ -332,229 +361,248 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
332
361
  "chan" : None,
333
362
  }
334
363
 
364
+ root_completion_list = {
365
+ "ver" : None,
366
+ "infos" : None,
367
+ "advert" : None,
368
+ "floodadv" : None,
369
+ "msg" : contact_list,
370
+ "wait_ack" : None,
371
+ "time" : None,
372
+ "clock" : {"sync" : None},
373
+ "reboot" : None,
374
+ "card" : None,
375
+ "upload_card" : None,
376
+ "contacts": None,
377
+ "pending_contacts": None,
378
+ "add_pending": pending_list,
379
+ "flush_pending": None,
380
+ "contact_info": contact_list,
381
+ "export_contact" : contact_list,
382
+ "upload_contact" : contact_list,
383
+ "share_contact" : contact_list,
384
+ "path": contact_list,
385
+ "disc_path" : contact_list,
386
+ "trace" : None,
387
+ "reset_path" : contact_list,
388
+ "change_path" : contact_list,
389
+ "change_flags" : contact_list,
390
+ "remove_contact" : contact_list,
391
+ "import_contact" : {"meshcore://":None},
392
+ "reload_contacts" : None,
393
+ "login" : contact_list,
394
+ "cmd" : contact_list,
395
+ "req_status" : contact_list,
396
+ "req_bstatus" : contact_list,
397
+ "logout" : contact_list,
398
+ "req_telemetry" : contact_list,
399
+ "req_binary" : contact_list,
400
+ "req_mma" : contact_list,
401
+ "self_telemetry" : None,
402
+ "get_channel": None,
403
+ "set_channel": None,
404
+ "get_channels": None,
405
+ "remove_channel": None,
406
+ "set" : {
407
+ "name" : None,
408
+ "pin" : None,
409
+ "radio" : {",,,":None, "f,bw,sf,cr":None},
410
+ "tx" : None,
411
+ "tuning" : {",", "af,tx_d"},
412
+ "lat" : None,
413
+ "lon" : None,
414
+ "coords" : None,
415
+ "print_snr" : {"on":None, "off": None},
416
+ "json_msgs" : {"on":None, "off": None},
417
+ "color" : {"on":None, "off":None},
418
+ "print_name" : {"on":None, "off":None},
419
+ "print_adverts" : {"on":None, "off":None},
420
+ "print_new_contacts" : {"on": None, "off":None},
421
+ "print_path_updates" : {"on":None,"off":None},
422
+ "classic_prompt" : {"on" : None, "off":None},
423
+ "manual_add_contacts" : {"on" : None, "off":None},
424
+ "telemetry_mode_base" : {"always" : None, "device":None, "never":None},
425
+ "telemetry_mode_loc" : {"always" : None, "device":None, "never":None},
426
+ "telemetry_mode_env" : {"always" : None, "device":None, "never":None},
427
+ "advert_loc_policy" : {"none" : None, "share" : None},
428
+ "auto_update_contacts" : {"on":None, "off":None},
429
+ "multi_acks" : {"on": None, "off":None},
430
+ "max_attempts" : None,
431
+ "max_flood_attempts" : None,
432
+ "flood_after" : None,
433
+ },
434
+ "get" : {"name":None,
435
+ "bat":None,
436
+ "fstats": None,
437
+ "radio":None,
438
+ "tx":None,
439
+ "coords":None,
440
+ "lat":None,
441
+ "lon":None,
442
+ "print_snr":None,
443
+ "json_msgs":None,
444
+ "color":None,
445
+ "print_name":None,
446
+ "print_adverts":None,
447
+ "print_path_updates":None,
448
+ "print_new_contacts":None,
449
+ "classic_prompt":None,
450
+ "manual_add_contacts":None,
451
+ "telemetry_mode_base":None,
452
+ "telemetry_mode_loc":None,
453
+ "telemetry_mode_env":None,
454
+ "advert_loc_policy":None,
455
+ "auto_update_contacts":None,
456
+ "multi_acks":None,
457
+ "max_attempts":None,
458
+ "max_flood_attempts":None,
459
+ "flood_after":None,
460
+ "custom":None,
461
+ },
462
+ }
463
+
464
+ contact_completion_list = {
465
+ "contact_info": None,
466
+ "export_contact" : None,
467
+ "share_contact" : None,
468
+ "upload_contact" : None,
469
+ "path": None,
470
+ "disc_path": None,
471
+ "trace": None,
472
+ "dtrace": None,
473
+ "reset_path" : None,
474
+ "change_path" : None,
475
+ "change_flags" : None,
476
+ "req_telemetry" : None,
477
+ "req_binary" : None,
478
+ "forget_password" : None,
479
+ }
480
+
481
+ client_completion_list = dict(contact_completion_list)
482
+ client_completion_list.update({
483
+ "get" : { "timeout":None, },
484
+ "set" : { "timeout":None, },
485
+ })
486
+
487
+ repeater_completion_list = dict(contact_completion_list)
488
+ repeater_completion_list.update({
489
+ "login" : None,
490
+ "logout" : None,
491
+ "req_status" : None,
492
+ "req_bstatus" : None,
493
+ "cmd" : None,
494
+ "ver" : None,
495
+ "advert" : None,
496
+ "time" : None,
497
+ "clock" : {"sync" : None},
498
+ "reboot" : None,
499
+ "start ota" : None,
500
+ "password" : None,
501
+ "neighbors" : None,
502
+ "req_acl":None,
503
+ "setperm":contact_list,
504
+ "gps" : {"on":None,"off":None,"sync":None,"setloc":None,
505
+ "advert" : {"none": None, "share": None, "prefs": None},
506
+ },
507
+ "sensor": {"list": None, "set": {"gps": None}, "get": {"gps": None}},
508
+ "get" : {"name" : None,
509
+ "role":None,
510
+ "radio" : None,
511
+ "freq":None,
512
+ "tx":None,
513
+ "af" : None,
514
+ "repeat" : None,
515
+ "allow.read.only" : None,
516
+ "flood.advert.interval" : None,
517
+ "flood.max":None,
518
+ "advert.interval" : None,
519
+ "guest.password" : None,
520
+ "rxdelay": None,
521
+ "txdelay": None,
522
+ "direct.tx_delay":None,
523
+ "public.key":None,
524
+ "lat" : None,
525
+ "lon" : None,
526
+ "telemetry" : None,
527
+ "status" : None,
528
+ "timeout" : None,
529
+ "acl":None,
530
+ "bridge.enabled":None,
531
+ "bridge.delay":None,
532
+ "bridge.source":None,
533
+ "bridge.baud":None,
534
+ "bridge.secret":None,
535
+ "bridge.type":None,
536
+ },
537
+ "set" : {"name" : None,
538
+ "radio" : {",,,":None, "f,bw,sf,cr": None},
539
+ "freq" : None,
540
+ "tx" : None,
541
+ "af": None,
542
+ "repeat" : {"on": None, "off": None},
543
+ "flood.advert.interval" : None,
544
+ "flood.max" : None,
545
+ "advert.interval" : None,
546
+ "guest.password" : None,
547
+ "allow.read.only" : {"on": None, "off": None},
548
+ "rxdelay" : None,
549
+ "txdelay": None,
550
+ "direct.txdelay" : None,
551
+ "lat" : None,
552
+ "lon" : None,
553
+ "timeout" : None,
554
+ "perm":contact_list,
555
+ "bridge.enabled":{"on": None, "off": None},
556
+ "bridge.delay":None,
557
+ "bridge.source":None,
558
+ "bridge.baud":None,
559
+ "bridge.secret":None,
560
+ },
561
+ "erase": None,
562
+ "log" : {"start" : None, "stop" : None, "erase" : None}
563
+ })
564
+
565
+ sensor_completion_list = dict(repeater_completion_list)
566
+ sensor_completion_list.update({"req_mma":{"begin end":None}})
567
+ sensor_completion_list["get"].update({ "mma":None, })
568
+
335
569
  if to is None :
336
- completion_list.update({
337
- "ver" : None,
338
- "infos" : None,
339
- "advert" : None,
340
- "floodadv" : None,
341
- "msg" : contact_list,
342
- "wait_ack" : None,
343
- "time" : None,
344
- "clock" : {"sync" : None},
345
- "reboot" : None,
346
- "card" : None,
347
- "upload_card" : None,
348
- "contacts": None,
349
- "pending_contacts": None,
350
- "add_pending": pending_list,
351
- "flush_pending": None,
352
- "contact_info": contact_list,
353
- "export_contact" : contact_list,
354
- "upload_contact" : contact_list,
355
- "share_contact" : contact_list,
356
- "path": contact_list,
357
- "disc_path" : contact_list,
358
- "trace" : None,
359
- "reset_path" : contact_list,
360
- "change_path" : contact_list,
361
- "change_flags" : contact_list,
362
- "remove_contact" : contact_list,
363
- "import_contact" : {"meshcore://":None},
364
- "reload_contacts" : None,
365
- "login" : contact_list,
366
- "cmd" : contact_list,
367
- "req_status" : contact_list,
368
- "req_bstatus" : contact_list,
369
- "logout" : contact_list,
370
- "req_telemetry" : contact_list,
371
- "req_binary" : contact_list,
372
- "req_mma" : contact_list,
373
- "self_telemetry" : None,
374
- "get_channel": None,
375
- "set_channel": None,
376
- "get_channels": None,
377
- "remove_channel": None,
378
- "set" : {
379
- "name" : None,
380
- "pin" : None,
381
- "radio" : {",,,":None, "f,bw,sf,cr":None},
382
- "tx" : None,
383
- "tuning" : {",", "af,tx_d"},
384
- "lat" : None,
385
- "lon" : None,
386
- "coords" : None,
387
- "print_snr" : {"on":None, "off": None},
388
- "json_msgs" : {"on":None, "off": None},
389
- "color" : {"on":None, "off":None},
390
- "print_name" : {"on":None, "off":None},
391
- "print_adverts" : {"on":None, "off":None},
392
- "print_new_contacts" : {"on": None, "off":None},
393
- "print_path_updates" : {"on":None,"off":None},
394
- "classic_prompt" : {"on" : None, "off":None},
395
- "manual_add_contacts" : {"on" : None, "off":None},
396
- "telemetry_mode_base" : {"always" : None, "device":None, "never":None},
397
- "telemetry_mode_loc" : {"always" : None, "device":None, "never":None},
398
- "telemetry_mode_env" : {"always" : None, "device":None, "never":None},
399
- "advert_loc_policy" : {"none" : None, "share" : None},
400
- "auto_update_contacts" : {"on":None, "off":None},
401
- "multi_acks" : {"on": None, "off":None},
402
- "max_attempts" : None,
403
- "max_flood_attempts" : None,
404
- "flood_after" : None,
405
- },
406
- "get" : {"name":None,
407
- "bat":None,
408
- "fstats": None,
409
- "radio":None,
410
- "tx":None,
411
- "coords":None,
412
- "lat":None,
413
- "lon":None,
414
- "print_snr":None,
415
- "json_msgs":None,
416
- "color":None,
417
- "print_name":None,
418
- "print_adverts":None,
419
- "print_path_updates":None,
420
- "print_new_contacts":None,
421
- "classic_prompt":None,
422
- "manual_add_contacts":None,
423
- "telemetry_mode_base":None,
424
- "telemetry_mode_loc":None,
425
- "telemetry_mode_env":None,
426
- "advert_loc_policy":None,
427
- "auto_update_contacts":None,
428
- "multi_acks":None,
429
- "max_attempts":None,
430
- "max_flood_attempts":None,
431
- "flood_after":None,
432
- "custom":None,
433
- },
434
- })
570
+ completion_list.update(dict(root_completion_list))
435
571
  completion_list["set"].update(make_completion_dict.custom_vars)
436
572
  completion_list["get"].update(make_completion_dict.custom_vars)
437
573
  else :
438
574
  completion_list.update({
439
575
  "send" : None,
440
576
  })
577
+ if to['type'] == 1 :
578
+ completion_list.update(client_completion_list)
579
+ if to['type'] == 2 or to['type'] == 3 : # repeaters and room servers
580
+ completion_list.update(repeater_completion_list)
581
+ if (to['type'] == 4) : #specific to sensors
582
+ completion_list.update(sensor_completion_list)
441
583
 
442
- if to['type'] > 0: # contact
443
- completion_list.update({
444
- "contact_info": None,
445
- "export_contact" : None,
446
- "share_contact" : None,
447
- "upload_contact" : None,
448
- "path": None,
449
- "disc_path": None,
450
- "trace": None,
451
- "dtrace": None,
452
- "reset_path" : None,
453
- "change_path" : None,
454
- "change_flags" : None,
455
- "req_telemetry" : None,
456
- "req_binary" : None,
457
- })
584
+ slash_root_completion_list = {}
585
+ for k,v in root_completion_list.items():
586
+ slash_root_completion_list["/"+k]=v
587
+
588
+ completion_list.update(slash_root_completion_list)
458
589
 
459
- if to['type'] == 1 :
460
- completion_list.update({
461
- "get" : {
462
- "timeout":None,
463
- },
464
- "set" : {
465
- "timeout":None,
466
- },
467
- })
468
-
469
- if to['type'] > 1 : # repeaters and room servers
470
- completion_list.update({
471
- "login" : None,
472
- "logout" : None,
473
- "req_status" : None,
474
- "req_bstatus" : None,
475
- "cmd" : None,
476
- "ver" : None,
477
- "advert" : None,
478
- "time" : None,
479
- "clock" : {"sync" : None},
480
- "reboot" : None,
481
- "start ota" : None,
482
- "password" : None,
483
- "neighbors" : None,
484
- "req_acl":None,
485
- "setperm":contact_list,
486
- "gps" : {"on":None,"off":None,"sync":None,"setloc":None,
487
- "advert" : {"none": None, "share": None, "prefs": None},
488
- },
489
- "sensor": {"list": None, "set": {"gps": None}, "get": {"gps": None}},
490
- "get" : {"name" : None,
491
- "role":None,
492
- "radio" : None,
493
- "freq":None,
494
- "tx":None,
495
- "af" : None,
496
- "repeat" : None,
497
- "allow.read.only" : None,
498
- "flood.advert.interval" : None,
499
- "flood.max":None,
500
- "advert.interval" : None,
501
- "guest.password" : None,
502
- "rxdelay": None,
503
- "txdelay": None,
504
- "direct.tx_delay":None,
505
- "public.key":None,
506
- "lat" : None,
507
- "lon" : None,
508
- "telemetry" : None,
509
- "status" : None,
510
- "timeout" : None,
511
- "acl":None,
512
- "bridge.enabled":None,
513
- "bridge.delay":None,
514
- "bridge.source":None,
515
- "bridge.baud":None,
516
- "bridge.secret":None,
517
- "bridge.type":None,
518
- },
519
- "set" : {"name" : None,
520
- "radio" : {",,,":None, "f,bw,sf,cr": None},
521
- "freq" : None,
522
- "tx" : None,
523
- "af": None,
524
- "repeat" : {"on": None, "off": None},
525
- "flood.advert.interval" : None,
526
- "flood.max" : None,
527
- "advert.interval" : None,
528
- "guest.password" : None,
529
- "allow.read.only" : {"on": None, "off": None},
530
- "rxdelay" : None,
531
- "txdelay": None,
532
- "direct.txdelay" : None,
533
- "lat" : None,
534
- "lon" : None,
535
- "timeout" : None,
536
- "perm":contact_list,
537
- "bridge.enabled":{"on": None, "off": None},
538
- "bridge.delay":None,
539
- "bridge.source":None,
540
- "bridge.baud":None,
541
- "bridge.secret":None,
542
- },
543
- "erase": None,
544
- "log" : {"start" : None, "stop" : None, "erase" : None}
545
- })
590
+ slash_contacts_completion_list = {}
591
+ for k,v in contacts.items():
592
+ d={}
593
+ if v["type"] == 1:
594
+ l = client_completion_list
595
+ elif v["type"] == 2 or v["type"] == 3:
596
+ l = repeater_completion_list
597
+ elif v["type"] == 4:
598
+ l = sensor_completion_list
546
599
 
547
- if (to['type'] == 4) : #specific to sensors
548
- completion_list.update({
549
- "req_mma":{"begin end":None},
550
- })
600
+ for kk, vv in l.items():
601
+ d["/" + v["adv_name"] + "/" + kk] = vv
551
602
 
552
- completion_list["get"].update({
553
- "mma":None,
554
- })
603
+ slash_contacts_completion_list.update(d)
555
604
 
556
- completion_list["set"].update({
557
- })
605
+ completion_list.update(slash_contacts_completion_list)
558
606
 
559
607
  completion_list.update({
560
608
  "script" : None,
@@ -573,7 +621,6 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
573
621
  contact = to
574
622
  prev_contact = None
575
623
 
576
- # await get_contacts(mc, anim=True)
577
624
  await get_contacts(mc, anim=True)
578
625
  await get_channels(mc, anim=True)
579
626
  await subscribe_to_msgs(mc, above=True)
@@ -665,7 +712,7 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
665
712
  session.app.ttimeoutlen = 0.2
666
713
  session.app.timeoutlen = 0.2
667
714
 
668
- completer = NestedCompleter.from_nested_dict(
715
+ completer = MyNestedCompleter.from_nested_dict(
669
716
  make_completion_dict(mc.contacts,
670
717
  mc.pending_contacts,
671
718
  to=contact,
@@ -684,6 +731,24 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
684
731
  args = shlex.split(line[1:])
685
732
  await process_cmds(mc, args)
686
733
 
734
+ elif line.startswith("/") :
735
+ path = line.split(" ", 1)[0]
736
+ if path.count("/") == 1:
737
+ args = shlex.split(line[1:])
738
+ await process_cmds(mc, args)
739
+ else:
740
+ cmdline = line[1:].split("/",1)[1]
741
+ contact_name = path[1:].split("/",1)[0]
742
+ tct = mc.get_contact_by_name(contact_name)
743
+ if tct is None:
744
+ print(f"{contact_name} is not a contact")
745
+ else:
746
+ if not await process_contact_chat_line(mc, tct, cmdline):
747
+ if tct["type"] == 1:
748
+ last_ack = await msg_ack(mc, tct, cmdline)
749
+ else :
750
+ await process_cmds(mc, ["cmd", tct["adv_name"], cmdline])
751
+
687
752
  elif line.startswith("to ") : # dest
688
753
  dest = line[3:]
689
754
  if dest.startswith("\"") or dest.startswith("\'") : # if name starts with a quote
@@ -706,7 +771,7 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
706
771
  elif dest == "!" :
707
772
  nc = process_event_message.last_node
708
773
  else :
709
- chan = await get_channel_by_name(mc, dest)
774
+ chan = await get_channel_by_name(mc, dest)
710
775
  if chan is None :
711
776
  print(f"Contact '{dest}' not found in contacts.")
712
777
  nc = contact
@@ -751,104 +816,8 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
751
816
  args = shlex.split(line)
752
817
  await process_cmds(mc, args)
753
818
 
754
- # commands that take contact as second arg will be sent to recipient
755
- elif contact["type"] > 0 and (line == "sc" or line == "share_contact" or\
756
- line == "ec" or line == "export_contact" or\
757
- line == "uc" or line == "upload_contact" or\
758
- line == "rp" or line == "reset_path" or\
759
- line == "dp" or line == "disc_path" or\
760
- line == "contact_info" or line == "ci" or\
761
- line == "req_status" or line == "rs" or\
762
- line == "req_bstatus" or line == "rbs" or\
763
- line == "req_telemetry" or line == "rt" or\
764
- line == "req_acl" or\
765
- line == "path" or\
766
- line == "logout" ) :
767
- args = [line, contact['adv_name']]
768
- await process_cmds(mc, args)
769
-
770
- elif contact["type"] > 0 and line.startswith("set timeout "):
771
- cmds=line.split(" ")
772
- contact["timeout"] = float(cmds[2])
773
-
774
- elif contact["type"] > 0 and line == "get timeout":
775
- print(f"timeout: {0 if not 'timeout' in contact else contact['timeout']}")
776
-
777
- elif contact["type"] == 4 and\
778
- (line.startswith("get mma ")) or\
779
- contact["type"] > 1 and\
780
- (line.startswith("get telemetry") or line.startswith("get status") or line.startswith("get acl")):
781
- cmds = line.split(" ")
782
- args = [f"req_{cmds[1]}", contact['adv_name']]
783
- if len(cmds) > 2 :
784
- args = args + cmds[2:]
785
- if line.startswith("get mma ") and len(args) < 4:
786
- args.append("0")
787
- await process_cmds(mc, args)
788
-
789
- # special treatment for setperm to support contact name as param
790
- elif contact["type"] > 1 and\
791
- (line.startswith("setperm ") or line.startswith("set perm ")):
792
- try:
793
- cmds = shlex.split(line)
794
- off = 1 if line.startswith("set perm") else 0
795
- name = cmds[1 + off]
796
- perm_string = cmds[2 + off]
797
- if (perm_string.startswith("0x")):
798
- perm = int(perm_string,0)
799
- elif (perm_string.startswith("#")):
800
- perm = int(perm_string[1:])
801
- else:
802
- perm = int(perm_string,16)
803
- ct=mc.get_contact_by_name(name)
804
- if ct is None:
805
- ct=mc.get_contact_by_key_prefix(name)
806
- if ct is None:
807
- if name == "self" or mc.self_info["public_key"].startswith(name):
808
- key = mc.self_info["public_key"]
809
- else:
810
- key = name
811
- else:
812
- key=ct["public_key"]
813
- newline=f"setperm {key} {perm}"
814
- await process_cmds(mc, ["cmd", contact["adv_name"], newline])
815
- except IndexError:
816
- print("Wrong number of parameters")
817
-
818
- # trace called on a contact
819
- elif contact["type"] > 0 and (
820
- line == "trace" or line == "tr") :
821
- await print_trace_to(mc, contact)
822
-
823
- elif contact["type"] > 0 and (
824
- line == "dtrace" or line == "dt") :
825
- await print_disc_trace_to(mc, contact)
826
-
827
- # same but for commands with a parameter
828
- elif contact["type"] > 0 and (line.startswith("cmd ") or\
829
- line.startswith("cp ") or line.startswith("change_path ") or\
830
- line.startswith("cf ") or line.startswith("change_flags ") or\
831
- line.startswith("req_binary ") or\
832
- line.startswith("login ")) :
833
- cmds = line.split(" ", 1)
834
- args = [cmds[0], contact['adv_name'], cmds[1]]
835
- await process_cmds(mc, args)
836
-
837
- elif contact["type"] == 4 and \
838
- (line.startswith("req_mma ") or line.startswith('rm ')) :
839
- cmds = line.split(" ")
840
- if len(cmds) < 3 :
841
- cmds.append("0")
842
- args = [cmds[0], contact['adv_name'], cmds[1], cmds[2]]
843
- await process_cmds(mc, args)
844
-
845
- elif line.startswith(":") : # : will send a command to current recipient
846
- args=["cmd", contact['adv_name'], line[1:]]
847
- await process_cmds(mc, args)
848
-
849
- elif line == "reset path" : # reset path for compat with terminal chat
850
- args = ["reset_path", contact['adv_name']]
851
- await process_cmds(mc, args)
819
+ elif await process_contact_chat_line(mc, contact, line):
820
+ pass
852
821
 
853
822
  elif line == "list" : # list command from chat displays contacts on a line
854
823
  it = iter(mc.contacts.items())
@@ -886,6 +855,165 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
886
855
  interactive_loop.classic = False
887
856
  interactive_loop.print_name = True
888
857
 
858
+ async def process_contact_chat_line(mc, contact, line):
859
+ if contact["type"] == 0:
860
+ return False
861
+
862
+ if line.startswith(":") : # : will send a command to current recipient
863
+ args=["cmd", contact['adv_name'], line[1:]]
864
+ await process_cmds(mc, args)
865
+ return True
866
+
867
+ if line == "reset path" : # reset path for compat with terminal chat
868
+ args = ["reset_path", contact['adv_name']]
869
+ await process_cmds(mc, args)
870
+ return True
871
+
872
+ # commands that take contact as second arg will be sent to recipient
873
+ if line == "sc" or line == "share_contact" or\
874
+ line == "ec" or line == "export_contact" or\
875
+ line == "uc" or line == "upload_contact" or\
876
+ line == "rp" or line == "reset_path" or\
877
+ line == "dp" or line == "disc_path" or\
878
+ line == "contact_info" or line == "ci" or\
879
+ line == "req_status" or line == "rs" or\
880
+ line == "req_bstatus" or line == "rbs" or\
881
+ line == "req_telemetry" or line == "rt" or\
882
+ line == "req_acl" or\
883
+ line == "path" or\
884
+ line == "logout" :
885
+ args = [line, contact['adv_name']]
886
+ await process_cmds(mc, args)
887
+ return True
888
+
889
+ # special case for rp that can be chained from cmdline
890
+ if line.startswith("rp ") or line.startswith("reset_path ") :
891
+ args = ["rp", contact['adv_name']]
892
+ await process_cmds(mc, args)
893
+ secline = line.split(" ", 1)[1]
894
+ await process_contact_chat_line(mc, contact, secline)
895
+ return True
896
+
897
+ if line.startswith("set timeout "):
898
+ cmds=line.split(" ")
899
+ contact["timeout"] = float(cmds[2])
900
+ return True
901
+
902
+ if line == "get timeout":
903
+ print(f"timeout: {0 if not 'timeout' in contact else contact['timeout']}")
904
+ return True
905
+
906
+ if contact["type"] == 4 and\
907
+ (line.startswith("get mma ")) or\
908
+ contact["type"] > 1 and\
909
+ (line.startswith("get telemetry") or line.startswith("get status") or line.startswith("get acl")):
910
+ cmds = line.split(" ")
911
+ args = [f"req_{cmds[1]}", contact['adv_name']]
912
+ if len(cmds) > 2 :
913
+ args = args + cmds[2:]
914
+ if line.startswith("get mma ") and len(args) < 4:
915
+ args.append("0")
916
+ await process_cmds(mc, args)
917
+ return True
918
+
919
+ # special treatment for setperm to support contact name as param
920
+ if contact["type"] > 1 and\
921
+ (line.startswith("setperm ") or line.startswith("set perm ")):
922
+ try:
923
+ cmds = shlex.split(line)
924
+ off = 1 if line.startswith("set perm") else 0
925
+ name = cmds[1 + off]
926
+ perm_string = cmds[2 + off]
927
+ if (perm_string.startswith("0x")):
928
+ perm = int(perm_string,0)
929
+ elif (perm_string.startswith("#")):
930
+ perm = int(perm_string[1:])
931
+ else:
932
+ perm = int(perm_string,16)
933
+ ct=mc.get_contact_by_name(name)
934
+ if ct is None:
935
+ ct=mc.get_contact_by_key_prefix(name)
936
+ if ct is None:
937
+ if name == "self" or mc.self_info["public_key"].startswith(name):
938
+ key = mc.self_info["public_key"]
939
+ else:
940
+ key = name
941
+ else:
942
+ key=ct["public_key"]
943
+ newline=f"setperm {key} {perm}"
944
+ await process_cmds(mc, ["cmd", contact["adv_name"], newline])
945
+ except IndexError:
946
+ print("Wrong number of parameters")
947
+ return True
948
+
949
+ # trace called on a contact
950
+ if line == "trace" or line == "tr" :
951
+ await print_trace_to(mc, contact)
952
+ return True
953
+
954
+ if line == "dtrace" or line == "dt" :
955
+ await print_disc_trace_to(mc, contact)
956
+ return True
957
+
958
+ # same but for commands with a parameter
959
+ if line.startswith("cmd ") or\
960
+ line.startswith("cp ") or line.startswith("change_path ") or\
961
+ line.startswith("cf ") or line.startswith("change_flags ") or\
962
+ line.startswith("req_binary ") or\
963
+ line.startswith("login ") :
964
+ cmds = line.split(" ", 1)
965
+ args = [cmds[0], contact['adv_name'], cmds[1]]
966
+ await process_cmds(mc, args)
967
+ return True
968
+
969
+ if line == "login": # use stored password or prompt for it
970
+ password_file = ""
971
+ password = ""
972
+ if os.path.isdir(MCCLI_CONFIG_DIR) :
973
+ password_file = MCCLI_CONFIG_DIR + contact['adv_name'] + ".pass"
974
+ if os.path.exists(password_file) :
975
+ with open(password_file, "r", encoding="utf-8") as f :
976
+ password=f.readline().strip()
977
+
978
+ if password == "":
979
+ try:
980
+ sess = PromptSession("Password: ", is_password=True)
981
+ password = await sess.prompt_async()
982
+ except EOFError:
983
+ logger.info("Canceled")
984
+ return True
985
+
986
+ if password_file != "":
987
+ with open(password_file, "w", encoding="utf-8") as f :
988
+ f.write(password)
989
+
990
+ args = ["login", contact['adv_name'], password]
991
+ await process_cmds(mc, args)
992
+ return True
993
+
994
+ if line.startswith("forget_password") or line.startswith("fp"):
995
+ password_file = MCCLI_CONFIG_DIR + contact['adv_name'] + ".pass"
996
+ if os.path.exists(password_file):
997
+ os.remove(password_file)
998
+ try:
999
+ secline = line.split(" ", 1)[1]
1000
+ await process_contact_chat_line(mc, contact, secline)
1001
+ except IndexError:
1002
+ pass
1003
+ return True
1004
+
1005
+ if contact["type"] == 4 and \
1006
+ (line.startswith("req_mma ") or line.startswith('rm ')) :
1007
+ cmds = line.split(" ")
1008
+ if len(cmds) < 3 :
1009
+ cmds.append("0")
1010
+ args = [cmds[0], contact['adv_name'], cmds[1], cmds[2]]
1011
+ await process_cmds(mc, args)
1012
+ return True
1013
+
1014
+ return False
1015
+
1016
+
889
1017
  async def send_cmd (mc, contact, cmd) :
890
1018
  res = await mc.commands.send_cmd(contact, cmd)
891
1019
  if not res is None and not res.type == EventType.ERROR:
@@ -909,7 +1037,7 @@ async def send_chan_msg(mc, nb, msg):
909
1037
  sent["text"] = msg
910
1038
  sent["txt_type"] = 0
911
1039
  sent["name"] = mc.self_info['name']
912
- await log_message(mc, sent)
1040
+ await log_message(mc, sent)
913
1041
  return res
914
1042
 
915
1043
  async def send_msg (mc, contact, msg) :
@@ -1002,6 +1130,9 @@ async def get_channel_by_name (mc, name):
1002
1130
  return None
1003
1131
 
1004
1132
  async def get_contacts (mc, anim=False, lastomod=0, timeout=5) :
1133
+ if mc._contacts:
1134
+ return
1135
+
1005
1136
  if anim:
1006
1137
  print("Fetching contacts ", end="", flush=True)
1007
1138
 
@@ -1020,7 +1151,7 @@ async def get_contacts (mc, anim=False, lastomod=0, timeout=5) :
1020
1151
  done, pending = await asyncio.wait(
1021
1152
  futures, timeout=timeout, return_when=asyncio.FIRST_COMPLETED
1022
1153
  )
1023
-
1154
+
1024
1155
  # Check if any future completed successfully
1025
1156
  if len(done) == 0:
1026
1157
  logger.debug("Timeout while getting contacts")
@@ -1040,7 +1171,7 @@ async def get_contacts (mc, anim=False, lastomod=0, timeout=5) :
1040
1171
  if anim:
1041
1172
  if event.type == EventType.CONTACTS:
1042
1173
  print ((len(event.payload)-contact_nb)*"." + " Done")
1043
- else :
1174
+ else :
1044
1175
  print(" Error")
1045
1176
  for future in pending:
1046
1177
  future.cancel()
@@ -1702,7 +1833,7 @@ async def next_cmd(mc, cmds, json_output=False):
1702
1833
  res = await set_channel(mc, cmds[1], cmds[2])
1703
1834
  elif len(cmds[3]) != 32:
1704
1835
  res = None
1705
- else:
1836
+ else:
1706
1837
  res = await set_channel(mc, cmds[1], cmds[2], bytes.fromhex(cmds[3]))
1707
1838
  if res is None:
1708
1839
  print("Error setting channel")
@@ -1722,8 +1853,8 @@ async def next_cmd(mc, cmds, json_output=False):
1722
1853
  case "msg" | "m" | "{" : # sends to a contact from name
1723
1854
  argnum = 2
1724
1855
  dest = None
1725
-
1726
- if len(cmds[1]) == 12: # possibly an hex prefix
1856
+
1857
+ if len(cmds[1]) == 12: # possibly an hex prefix
1727
1858
  try:
1728
1859
  dest = bytes.fromhex(cmds[1])
1729
1860
  except ValueError:
@@ -1800,11 +1931,18 @@ async def next_cmd(mc, cmds, json_output=False):
1800
1931
 
1801
1932
  case "trace" | "tr":
1802
1933
  argnum = 1
1803
- res = await mc.commands.send_trace(path=cmds[1])
1934
+ path = cmds[1]
1935
+ plen = int(len(path)/2)
1936
+ if plen > 1 and path.count(",") == 0:
1937
+ path = cmds[1][0:2]
1938
+ for i in range(1, plen):
1939
+ path = path + "," + cmds[1][2*i:2*i+2]
1940
+
1941
+ res = await mc.commands.send_trace(path=path)
1804
1942
  if res and res.type != EventType.ERROR:
1805
1943
  tag= int.from_bytes(res.payload['expected_ack'], byteorder="little")
1806
1944
  timeout = res.payload["suggested_timeout"] / 1000 * 1.2
1807
- ev = await mc.wait_for_event(EventType.TRACE_DATA,
1945
+ ev = await mc.wait_for_event(EventType.TRACE_DATA,
1808
1946
  attribute_filters={"tag": tag},
1809
1947
  timeout=timeout)
1810
1948
  if ev is None:
@@ -1856,7 +1994,12 @@ async def next_cmd(mc, cmds, json_output=False):
1856
1994
  else:
1857
1995
  print(f"Unknown contact {cmds[1]}")
1858
1996
  else:
1859
- res = await mc.commands.send_login(contact, cmds[2])
1997
+ password = cmds[2]
1998
+ if password == "$":
1999
+ sess = PromptSession("Password: ", is_password=True)
2000
+ password = await sess.prompt_async()
2001
+
2002
+ res = await mc.commands.send_login(contact, password)
1860
2003
  logger.debug(res)
1861
2004
  if res.type == EventType.ERROR:
1862
2005
  if json_output :
@@ -1938,7 +2081,7 @@ async def next_cmd(mc, cmds, json_output=False):
1938
2081
  print("Timeout waiting telemetry")
1939
2082
  else :
1940
2083
  print(json.dumps(res.payload, indent=4))
1941
-
2084
+
1942
2085
  case "disc_path" | "dp" :
1943
2086
  argnum = 1
1944
2087
  await mc.ensure_contacts()
@@ -2438,6 +2581,9 @@ async def next_cmd(mc, cmds, json_output=False):
2438
2581
  except IndexError:
2439
2582
  logger.error("Error in parameters, returning")
2440
2583
  return None
2584
+ except EOFError:
2585
+ logger.error("Cancelled")
2586
+ return None
2441
2587
 
2442
2588
  async def process_cmds (mc, args, json_output=False) :
2443
2589
  cmds = args
@@ -2568,7 +2714,7 @@ async def main(argv):
2568
2714
  with open(MCCLI_ADDRESS, encoding="utf-8") as f :
2569
2715
  address = f.readline().strip()
2570
2716
 
2571
- opts, args = getopt.getopt(argv, "a:d:s:ht:p:b:jDhvSlT:P")
2717
+ opts, args = getopt.getopt(argv, "a:d:s:ht:p:b:fjDhvSlT:P")
2572
2718
  for opt, arg in opts :
2573
2719
  match opt:
2574
2720
  case "-d" : # name specified on cmdline
@@ -2598,25 +2744,34 @@ async def main(argv):
2598
2744
  case "-v":
2599
2745
  version()
2600
2746
  return
2747
+ case "-f": # connect to first encountered device
2748
+ address = ""
2601
2749
  case "-l" :
2602
2750
  print("BLE devices:")
2603
- devices = await BleakScanner.discover(timeout=timeout)
2604
- if len(devices) == 0:
2605
- print(" No ble device found")
2606
- for d in devices :
2607
- if not d.name is None and d.name.startswith("MeshCore-"):
2608
- print(f" {d.address} {d.name}")
2751
+ try :
2752
+ devices = await BleakScanner.discover(timeout=timeout)
2753
+ if len(devices) == 0:
2754
+ print(" No ble device found")
2755
+ for d in devices :
2756
+ if not d.name is None and d.name.startswith("MeshCore-"):
2757
+ print(f" {d.address} {d.name}")
2758
+ except BleakError:
2759
+ print(" No BLE HW")
2609
2760
  print("\nSerial ports:")
2610
2761
  ports = serial.tools.list_ports.comports()
2611
2762
  for port, desc, hwid in sorted(ports):
2612
2763
  print(f" {port:<18} {desc} [{hwid}]")
2613
2764
  return
2614
2765
  case "-S" :
2615
- devices = await BleakScanner.discover(timeout=timeout)
2616
2766
  choices = []
2617
- for d in devices:
2618
- if not d.name is None and d.name.startswith("MeshCore-"):
2619
- choices.append(({"type":"ble","device":d}, f"{d.address:<22} {d.name}"))
2767
+
2768
+ try :
2769
+ devices = await BleakScanner.discover(timeout=timeout)
2770
+ for d in devices:
2771
+ if not d.name is None and d.name.startswith("MeshCore-"):
2772
+ choices.append(({"type":"ble","device":d}, f"{d.address:<22} {d.name}"))
2773
+ except BleakError:
2774
+ logger.info("No BLE Device")
2620
2775
 
2621
2776
  ports = serial.tools.list_ports.comports()
2622
2777
  for port, desc, hwid in sorted(ports):
@@ -2642,7 +2797,7 @@ async def main(argv):
2642
2797
  else:
2643
2798
  logger.error("Invalid choice")
2644
2799
  return
2645
-
2800
+
2646
2801
  if (debug==True):
2647
2802
  logger.setLevel(logging.DEBUG)
2648
2803
  elif (json_output) :
@@ -2660,7 +2815,10 @@ async def main(argv):
2660
2815
  elif address and len(address) == 36 and len(address.split("-")) == 5:
2661
2816
  client = BleakClient(address) # mac uses uuid, we'll pass a client
2662
2817
  else:
2663
- logger.info(f"Scanning BLE for device matching {address}")
2818
+ if address == "":
2819
+ logger.info(f"Searching first MC BLE device")
2820
+ else:
2821
+ logger.info(f"Scanning BLE for device matching {address}")
2664
2822
  devices = await BleakScanner.discover(timeout=timeout)
2665
2823
  found = False
2666
2824
  for d in devices:
@@ -2685,6 +2843,26 @@ async def main(argv):
2685
2843
  mc = await MeshCore.create_ble(address=address, device=device, client=client, debug=debug, only_error=json_output, pin=pin)
2686
2844
  except ConnectionError :
2687
2845
  logger.info("Error while connecting, retrying once ...")
2846
+ if device is None or client is None: # Search for device
2847
+ logger.info(f"Scanning BLE for device matching {address}")
2848
+ devices = await BleakScanner.discover(timeout=timeout)
2849
+ found = False
2850
+ for d in devices:
2851
+ if not d.name is None and d.name.startswith("MeshCore-") and\
2852
+ (address is None or address in d.name) :
2853
+ address=d.address
2854
+ device=d
2855
+ logger.info(f"Found device {d.name} {d.address}")
2856
+ found = True
2857
+ break
2858
+ elif d.address == address : # on a mac, address is an uuid
2859
+ device = d
2860
+ logger.info(f"Found device {d.name} {d.address}")
2861
+ found = True
2862
+ break
2863
+ if not found :
2864
+ logger.info(f"Couldn't find device {address}")
2865
+ return
2688
2866
  try :
2689
2867
  mc = await MeshCore.create_ble(address=address, device=device, client=client, debug=debug, only_error=json_output, pin=pin)
2690
2868
  except ConnectionError :
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcore-cli
3
- Version: 1.1.40
3
+ Version: 1.2.0
4
4
  Summary: Command line interface to meshcore companion radios
5
5
  Project-URL: Homepage, https://github.com/fdlamotte/meshcore-cli
6
6
  Project-URL: Issues, https://github.com/fdlamotte/meshcore-cli/issues
@@ -0,0 +1,8 @@
1
+ meshcore_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ meshcore_cli/__main__.py,sha256=PfYgibmu2LEtC-OV7L1UgmvV3swJ5rQ4bbXHlwUFlgE,83
3
+ meshcore_cli/meshcore_cli.py,sha256=xvBECDCbnHtrK7VAEsQNHcYB1T9k3dQyJiAf_0VSct4,120884
4
+ meshcore_cli-1.2.0.dist-info/METADATA,sha256=OTBsXmevebzoVaxfVclgLgkkXWRYzQr5zySZUCAZd2k,11629
5
+ meshcore_cli-1.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
6
+ meshcore_cli-1.2.0.dist-info/entry_points.txt,sha256=77V29Pyth11GteDk7tneBN3MMk8JI7bTlS-BGSmxCmI,103
7
+ meshcore_cli-1.2.0.dist-info/licenses/LICENSE,sha256=F9s987VtS0AKxW7LdB2EkLMkrdeERI7ICdLJR60A9M4,1066
8
+ meshcore_cli-1.2.0.dist-info/RECORD,,
@@ -1,8 +0,0 @@
1
- meshcore_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- meshcore_cli/__main__.py,sha256=PfYgibmu2LEtC-OV7L1UgmvV3swJ5rQ4bbXHlwUFlgE,83
3
- meshcore_cli/meshcore_cli.py,sha256=oWPnTuetuus4oaP-QQlNJx7IIeRRYGCXUG4FSYTF7AM,116312
4
- meshcore_cli-1.1.40.dist-info/METADATA,sha256=wsHZfVpYP4kvQMI2KvKyHbygTO-Sgq_-8Nr6ghnnPrk,11630
5
- meshcore_cli-1.1.40.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
6
- meshcore_cli-1.1.40.dist-info/entry_points.txt,sha256=77V29Pyth11GteDk7tneBN3MMk8JI7bTlS-BGSmxCmI,103
7
- meshcore_cli-1.1.40.dist-info/licenses/LICENSE,sha256=F9s987VtS0AKxW7LdB2EkLMkrdeERI7ICdLJR60A9M4,1066
8
- meshcore_cli-1.1.40.dist-info/RECORD,,