meshcore-cli 1.1.41__tar.gz → 1.2.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcore-cli
3
- Version: 1.1.41
3
+ Version: 1.2.1
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "meshcore-cli"
7
- version = "1.1.41"
7
+ version = "1.2.1"
8
8
  authors = [
9
9
  { name="Florent de Lamotte", email="florent@frizoncorrea.fr" },
10
10
  ]
@@ -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
@@ -16,15 +17,20 @@ import traceback
16
17
  from prompt_toolkit.shortcuts import PromptSession
17
18
  from prompt_toolkit.shortcuts import CompleteStyle
18
19
  from prompt_toolkit.completion import NestedCompleter
20
+ from prompt_toolkit.completion import CompleteEvent, Completer, Completion
19
21
  from prompt_toolkit.history import FileHistory
20
22
  from prompt_toolkit.formatted_text import ANSI
21
23
  from prompt_toolkit.key_binding import KeyBindings
22
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
23
29
 
24
30
  from meshcore import MeshCore, EventType, logger
25
31
 
26
32
  # Version
27
- VERSION = "v1.1.41"
33
+ VERSION = "v1.2.1"
28
34
 
29
35
  # default ble address is stored in a config file
30
36
  MCCLI_CONFIG_DIR = str(Path.home()) + "/.config/meshcore/"
@@ -297,6 +303,29 @@ async def subscribe_to_msgs(mc, json_output=False, above=False):
297
303
  CS = mc.subscribe(EventType.CHANNEL_MSG_RECV, handle_message)
298
304
  await mc.start_auto_message_fetching()
299
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
+
300
329
  def make_completion_dict(contacts, pending={}, to=None, channels=None):
301
330
  contact_list = {}
302
331
  pending_list = {}
@@ -307,7 +336,6 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
307
336
  if not process_event_message.last_node is None:
308
337
  to_list["!"] = None
309
338
  to_list[".."] = None
310
- to_list["public"] = None
311
339
 
312
340
  it = iter(contacts.items())
313
341
  for c in it :
@@ -333,229 +361,248 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
333
361
  "chan" : None,
334
362
  }
335
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
+
336
569
  if to is None :
337
- completion_list.update({
338
- "ver" : None,
339
- "infos" : None,
340
- "advert" : None,
341
- "floodadv" : None,
342
- "msg" : contact_list,
343
- "wait_ack" : None,
344
- "time" : None,
345
- "clock" : {"sync" : None},
346
- "reboot" : None,
347
- "card" : None,
348
- "upload_card" : None,
349
- "contacts": None,
350
- "pending_contacts": None,
351
- "add_pending": pending_list,
352
- "flush_pending": None,
353
- "contact_info": contact_list,
354
- "export_contact" : contact_list,
355
- "upload_contact" : contact_list,
356
- "share_contact" : contact_list,
357
- "path": contact_list,
358
- "disc_path" : contact_list,
359
- "trace" : None,
360
- "reset_path" : contact_list,
361
- "change_path" : contact_list,
362
- "change_flags" : contact_list,
363
- "remove_contact" : contact_list,
364
- "import_contact" : {"meshcore://":None},
365
- "reload_contacts" : None,
366
- "login" : contact_list,
367
- "cmd" : contact_list,
368
- "req_status" : contact_list,
369
- "req_bstatus" : contact_list,
370
- "logout" : contact_list,
371
- "req_telemetry" : contact_list,
372
- "req_binary" : contact_list,
373
- "req_mma" : contact_list,
374
- "self_telemetry" : None,
375
- "get_channel": None,
376
- "set_channel": None,
377
- "get_channels": None,
378
- "remove_channel": None,
379
- "set" : {
380
- "name" : None,
381
- "pin" : None,
382
- "radio" : {",,,":None, "f,bw,sf,cr":None},
383
- "tx" : None,
384
- "tuning" : {",", "af,tx_d"},
385
- "lat" : None,
386
- "lon" : None,
387
- "coords" : None,
388
- "print_snr" : {"on":None, "off": None},
389
- "json_msgs" : {"on":None, "off": None},
390
- "color" : {"on":None, "off":None},
391
- "print_name" : {"on":None, "off":None},
392
- "print_adverts" : {"on":None, "off":None},
393
- "print_new_contacts" : {"on": None, "off":None},
394
- "print_path_updates" : {"on":None,"off":None},
395
- "classic_prompt" : {"on" : None, "off":None},
396
- "manual_add_contacts" : {"on" : None, "off":None},
397
- "telemetry_mode_base" : {"always" : None, "device":None, "never":None},
398
- "telemetry_mode_loc" : {"always" : None, "device":None, "never":None},
399
- "telemetry_mode_env" : {"always" : None, "device":None, "never":None},
400
- "advert_loc_policy" : {"none" : None, "share" : None},
401
- "auto_update_contacts" : {"on":None, "off":None},
402
- "multi_acks" : {"on": None, "off":None},
403
- "max_attempts" : None,
404
- "max_flood_attempts" : None,
405
- "flood_after" : None,
406
- },
407
- "get" : {"name":None,
408
- "bat":None,
409
- "fstats": None,
410
- "radio":None,
411
- "tx":None,
412
- "coords":None,
413
- "lat":None,
414
- "lon":None,
415
- "print_snr":None,
416
- "json_msgs":None,
417
- "color":None,
418
- "print_name":None,
419
- "print_adverts":None,
420
- "print_path_updates":None,
421
- "print_new_contacts":None,
422
- "classic_prompt":None,
423
- "manual_add_contacts":None,
424
- "telemetry_mode_base":None,
425
- "telemetry_mode_loc":None,
426
- "telemetry_mode_env":None,
427
- "advert_loc_policy":None,
428
- "auto_update_contacts":None,
429
- "multi_acks":None,
430
- "max_attempts":None,
431
- "max_flood_attempts":None,
432
- "flood_after":None,
433
- "custom":None,
434
- },
435
- })
570
+ completion_list.update(dict(root_completion_list))
436
571
  completion_list["set"].update(make_completion_dict.custom_vars)
437
572
  completion_list["get"].update(make_completion_dict.custom_vars)
438
573
  else :
439
574
  completion_list.update({
440
575
  "send" : None,
441
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)
442
583
 
443
- if to['type'] > 0: # contact
444
- completion_list.update({
445
- "contact_info": None,
446
- "export_contact" : None,
447
- "share_contact" : None,
448
- "upload_contact" : None,
449
- "path": None,
450
- "disc_path": None,
451
- "trace": None,
452
- "dtrace": None,
453
- "reset_path" : None,
454
- "change_path" : None,
455
- "change_flags" : None,
456
- "req_telemetry" : None,
457
- "req_binary" : None,
458
- })
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)
459
589
 
460
- if to['type'] == 1 :
461
- completion_list.update({
462
- "get" : {
463
- "timeout":None,
464
- },
465
- "set" : {
466
- "timeout":None,
467
- },
468
- })
469
-
470
- if to['type'] > 1 : # repeaters and room servers
471
- completion_list.update({
472
- "login" : None,
473
- "logout" : None,
474
- "req_status" : None,
475
- "req_bstatus" : None,
476
- "cmd" : None,
477
- "ver" : None,
478
- "advert" : None,
479
- "time" : None,
480
- "clock" : {"sync" : None},
481
- "reboot" : None,
482
- "start ota" : None,
483
- "password" : None,
484
- "neighbors" : None,
485
- "req_acl":None,
486
- "setperm":contact_list,
487
- "gps" : {"on":None,"off":None,"sync":None,"setloc":None,
488
- "advert" : {"none": None, "share": None, "prefs": None},
489
- },
490
- "sensor": {"list": None, "set": {"gps": None}, "get": {"gps": None}},
491
- "get" : {"name" : None,
492
- "role":None,
493
- "radio" : None,
494
- "freq":None,
495
- "tx":None,
496
- "af" : None,
497
- "repeat" : None,
498
- "allow.read.only" : None,
499
- "flood.advert.interval" : None,
500
- "flood.max":None,
501
- "advert.interval" : None,
502
- "guest.password" : None,
503
- "rxdelay": None,
504
- "txdelay": None,
505
- "direct.tx_delay":None,
506
- "public.key":None,
507
- "lat" : None,
508
- "lon" : None,
509
- "telemetry" : None,
510
- "status" : None,
511
- "timeout" : None,
512
- "acl":None,
513
- "bridge.enabled":None,
514
- "bridge.delay":None,
515
- "bridge.source":None,
516
- "bridge.baud":None,
517
- "bridge.secret":None,
518
- "bridge.type":None,
519
- },
520
- "set" : {"name" : None,
521
- "radio" : {",,,":None, "f,bw,sf,cr": None},
522
- "freq" : None,
523
- "tx" : None,
524
- "af": None,
525
- "repeat" : {"on": None, "off": None},
526
- "flood.advert.interval" : None,
527
- "flood.max" : None,
528
- "advert.interval" : None,
529
- "guest.password" : None,
530
- "allow.read.only" : {"on": None, "off": None},
531
- "rxdelay" : None,
532
- "txdelay": None,
533
- "direct.txdelay" : None,
534
- "lat" : None,
535
- "lon" : None,
536
- "timeout" : None,
537
- "perm":contact_list,
538
- "bridge.enabled":{"on": None, "off": None},
539
- "bridge.delay":None,
540
- "bridge.source":None,
541
- "bridge.baud":None,
542
- "bridge.secret":None,
543
- },
544
- "erase": None,
545
- "log" : {"start" : None, "stop" : None, "erase" : None}
546
- })
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
547
599
 
548
- if (to['type'] == 4) : #specific to sensors
549
- completion_list.update({
550
- "req_mma":{"begin end":None},
551
- })
600
+ for kk, vv in l.items():
601
+ d["/" + v["adv_name"] + "/" + kk] = vv
552
602
 
553
- completion_list["get"].update({
554
- "mma":None,
555
- })
603
+ slash_contacts_completion_list.update(d)
556
604
 
557
- completion_list["set"].update({
558
- })
605
+ completion_list.update(slash_contacts_completion_list)
559
606
 
560
607
  completion_list.update({
561
608
  "script" : None,
@@ -574,7 +621,6 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
574
621
  contact = to
575
622
  prev_contact = None
576
623
 
577
- # await get_contacts(mc, anim=True)
578
624
  await get_contacts(mc, anim=True)
579
625
  await get_channels(mc, anim=True)
580
626
  await subscribe_to_msgs(mc, above=True)
@@ -666,7 +712,7 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
666
712
  session.app.ttimeoutlen = 0.2
667
713
  session.app.timeoutlen = 0.2
668
714
 
669
- completer = NestedCompleter.from_nested_dict(
715
+ completer = MyNestedCompleter.from_nested_dict(
670
716
  make_completion_dict(mc.contacts,
671
717
  mc.pending_contacts,
672
718
  to=contact,
@@ -685,6 +731,24 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
685
731
  args = shlex.split(line[1:])
686
732
  await process_cmds(mc, args)
687
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
+
688
752
  elif line.startswith("to ") : # dest
689
753
  dest = line[3:]
690
754
  if dest.startswith("\"") or dest.startswith("\'") : # if name starts with a quote
@@ -707,7 +771,7 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
707
771
  elif dest == "!" :
708
772
  nc = process_event_message.last_node
709
773
  else :
710
- chan = await get_channel_by_name(mc, dest)
774
+ chan = await get_channel_by_name(mc, dest)
711
775
  if chan is None :
712
776
  print(f"Contact '{dest}' not found in contacts.")
713
777
  nc = contact
@@ -752,104 +816,8 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
752
816
  args = shlex.split(line)
753
817
  await process_cmds(mc, args)
754
818
 
755
- # commands that take contact as second arg will be sent to recipient
756
- elif contact["type"] > 0 and (line == "sc" or line == "share_contact" or\
757
- line == "ec" or line == "export_contact" or\
758
- line == "uc" or line == "upload_contact" or\
759
- line == "rp" or line == "reset_path" or\
760
- line == "dp" or line == "disc_path" or\
761
- line == "contact_info" or line == "ci" or\
762
- line == "req_status" or line == "rs" or\
763
- line == "req_bstatus" or line == "rbs" or\
764
- line == "req_telemetry" or line == "rt" or\
765
- line == "req_acl" or\
766
- line == "path" or\
767
- line == "logout" ) :
768
- args = [line, contact['adv_name']]
769
- await process_cmds(mc, args)
770
-
771
- elif contact["type"] > 0 and line.startswith("set timeout "):
772
- cmds=line.split(" ")
773
- contact["timeout"] = float(cmds[2])
774
-
775
- elif contact["type"] > 0 and line == "get timeout":
776
- print(f"timeout: {0 if not 'timeout' in contact else contact['timeout']}")
777
-
778
- elif contact["type"] == 4 and\
779
- (line.startswith("get mma ")) or\
780
- contact["type"] > 1 and\
781
- (line.startswith("get telemetry") or line.startswith("get status") or line.startswith("get acl")):
782
- cmds = line.split(" ")
783
- args = [f"req_{cmds[1]}", contact['adv_name']]
784
- if len(cmds) > 2 :
785
- args = args + cmds[2:]
786
- if line.startswith("get mma ") and len(args) < 4:
787
- args.append("0")
788
- await process_cmds(mc, args)
789
-
790
- # special treatment for setperm to support contact name as param
791
- elif contact["type"] > 1 and\
792
- (line.startswith("setperm ") or line.startswith("set perm ")):
793
- try:
794
- cmds = shlex.split(line)
795
- off = 1 if line.startswith("set perm") else 0
796
- name = cmds[1 + off]
797
- perm_string = cmds[2 + off]
798
- if (perm_string.startswith("0x")):
799
- perm = int(perm_string,0)
800
- elif (perm_string.startswith("#")):
801
- perm = int(perm_string[1:])
802
- else:
803
- perm = int(perm_string,16)
804
- ct=mc.get_contact_by_name(name)
805
- if ct is None:
806
- ct=mc.get_contact_by_key_prefix(name)
807
- if ct is None:
808
- if name == "self" or mc.self_info["public_key"].startswith(name):
809
- key = mc.self_info["public_key"]
810
- else:
811
- key = name
812
- else:
813
- key=ct["public_key"]
814
- newline=f"setperm {key} {perm}"
815
- await process_cmds(mc, ["cmd", contact["adv_name"], newline])
816
- except IndexError:
817
- print("Wrong number of parameters")
818
-
819
- # trace called on a contact
820
- elif contact["type"] > 0 and (
821
- line == "trace" or line == "tr") :
822
- await print_trace_to(mc, contact)
823
-
824
- elif contact["type"] > 0 and (
825
- line == "dtrace" or line == "dt") :
826
- await print_disc_trace_to(mc, contact)
827
-
828
- # same but for commands with a parameter
829
- elif contact["type"] > 0 and (line.startswith("cmd ") or\
830
- line.startswith("cp ") or line.startswith("change_path ") or\
831
- line.startswith("cf ") or line.startswith("change_flags ") or\
832
- line.startswith("req_binary ") or\
833
- line.startswith("login ")) :
834
- cmds = line.split(" ", 1)
835
- args = [cmds[0], contact['adv_name'], cmds[1]]
836
- await process_cmds(mc, args)
837
-
838
- elif contact["type"] == 4 and \
839
- (line.startswith("req_mma ") or line.startswith('rm ')) :
840
- cmds = line.split(" ")
841
- if len(cmds) < 3 :
842
- cmds.append("0")
843
- args = [cmds[0], contact['adv_name'], cmds[1], cmds[2]]
844
- await process_cmds(mc, args)
845
-
846
- elif line.startswith(":") : # : will send a command to current recipient
847
- args=["cmd", contact['adv_name'], line[1:]]
848
- await process_cmds(mc, args)
849
-
850
- elif line == "reset path" : # reset path for compat with terminal chat
851
- args = ["reset_path", contact['adv_name']]
852
- await process_cmds(mc, args)
819
+ elif await process_contact_chat_line(mc, contact, line):
820
+ pass
853
821
 
854
822
  elif line == "list" : # list command from chat displays contacts on a line
855
823
  it = iter(mc.contacts.items())
@@ -887,6 +855,165 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
887
855
  interactive_loop.classic = False
888
856
  interactive_loop.print_name = True
889
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
+
890
1017
  async def send_cmd (mc, contact, cmd) :
891
1018
  res = await mc.commands.send_cmd(contact, cmd)
892
1019
  if not res is None and not res.type == EventType.ERROR:
@@ -910,7 +1037,7 @@ async def send_chan_msg(mc, nb, msg):
910
1037
  sent["text"] = msg
911
1038
  sent["txt_type"] = 0
912
1039
  sent["name"] = mc.self_info['name']
913
- await log_message(mc, sent)
1040
+ await log_message(mc, sent)
914
1041
  return res
915
1042
 
916
1043
  async def send_msg (mc, contact, msg) :
@@ -1003,6 +1130,9 @@ async def get_channel_by_name (mc, name):
1003
1130
  return None
1004
1131
 
1005
1132
  async def get_contacts (mc, anim=False, lastomod=0, timeout=5) :
1133
+ if mc._contacts:
1134
+ return
1135
+
1006
1136
  if anim:
1007
1137
  print("Fetching contacts ", end="", flush=True)
1008
1138
 
@@ -1021,7 +1151,7 @@ async def get_contacts (mc, anim=False, lastomod=0, timeout=5) :
1021
1151
  done, pending = await asyncio.wait(
1022
1152
  futures, timeout=timeout, return_when=asyncio.FIRST_COMPLETED
1023
1153
  )
1024
-
1154
+
1025
1155
  # Check if any future completed successfully
1026
1156
  if len(done) == 0:
1027
1157
  logger.debug("Timeout while getting contacts")
@@ -1041,7 +1171,7 @@ async def get_contacts (mc, anim=False, lastomod=0, timeout=5) :
1041
1171
  if anim:
1042
1172
  if event.type == EventType.CONTACTS:
1043
1173
  print ((len(event.payload)-contact_nb)*"." + " Done")
1044
- else :
1174
+ else :
1045
1175
  print(" Error")
1046
1176
  for future in pending:
1047
1177
  future.cancel()
@@ -1703,7 +1833,7 @@ async def next_cmd(mc, cmds, json_output=False):
1703
1833
  res = await set_channel(mc, cmds[1], cmds[2])
1704
1834
  elif len(cmds[3]) != 32:
1705
1835
  res = None
1706
- else:
1836
+ else:
1707
1837
  res = await set_channel(mc, cmds[1], cmds[2], bytes.fromhex(cmds[3]))
1708
1838
  if res is None:
1709
1839
  print("Error setting channel")
@@ -1723,8 +1853,8 @@ async def next_cmd(mc, cmds, json_output=False):
1723
1853
  case "msg" | "m" | "{" : # sends to a contact from name
1724
1854
  argnum = 2
1725
1855
  dest = None
1726
-
1727
- if len(cmds[1]) == 12: # possibly an hex prefix
1856
+
1857
+ if len(cmds[1]) == 12: # possibly an hex prefix
1728
1858
  try:
1729
1859
  dest = bytes.fromhex(cmds[1])
1730
1860
  except ValueError:
@@ -1801,11 +1931,18 @@ async def next_cmd(mc, cmds, json_output=False):
1801
1931
 
1802
1932
  case "trace" | "tr":
1803
1933
  argnum = 1
1804
- 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)
1805
1942
  if res and res.type != EventType.ERROR:
1806
1943
  tag= int.from_bytes(res.payload['expected_ack'], byteorder="little")
1807
1944
  timeout = res.payload["suggested_timeout"] / 1000 * 1.2
1808
- ev = await mc.wait_for_event(EventType.TRACE_DATA,
1945
+ ev = await mc.wait_for_event(EventType.TRACE_DATA,
1809
1946
  attribute_filters={"tag": tag},
1810
1947
  timeout=timeout)
1811
1948
  if ev is None:
@@ -1857,7 +1994,12 @@ async def next_cmd(mc, cmds, json_output=False):
1857
1994
  else:
1858
1995
  print(f"Unknown contact {cmds[1]}")
1859
1996
  else:
1860
- 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)
1861
2003
  logger.debug(res)
1862
2004
  if res.type == EventType.ERROR:
1863
2005
  if json_output :
@@ -1939,7 +2081,7 @@ async def next_cmd(mc, cmds, json_output=False):
1939
2081
  print("Timeout waiting telemetry")
1940
2082
  else :
1941
2083
  print(json.dumps(res.payload, indent=4))
1942
-
2084
+
1943
2085
  case "disc_path" | "dp" :
1944
2086
  argnum = 1
1945
2087
  await mc.ensure_contacts()
@@ -2439,6 +2581,9 @@ async def next_cmd(mc, cmds, json_output=False):
2439
2581
  except IndexError:
2440
2582
  logger.error("Error in parameters, returning")
2441
2583
  return None
2584
+ except EOFError:
2585
+ logger.error("Cancelled")
2586
+ return None
2442
2587
 
2443
2588
  async def process_cmds (mc, args, json_output=False) :
2444
2589
  cmds = args
@@ -2569,7 +2714,7 @@ async def main(argv):
2569
2714
  with open(MCCLI_ADDRESS, encoding="utf-8") as f :
2570
2715
  address = f.readline().strip()
2571
2716
 
2572
- 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")
2573
2718
  for opt, arg in opts :
2574
2719
  match opt:
2575
2720
  case "-d" : # name specified on cmdline
@@ -2599,6 +2744,8 @@ async def main(argv):
2599
2744
  case "-v":
2600
2745
  version()
2601
2746
  return
2747
+ case "-f": # connect to first encountered device
2748
+ address = ""
2602
2749
  case "-l" :
2603
2750
  print("BLE devices:")
2604
2751
  try :
@@ -2609,7 +2756,7 @@ async def main(argv):
2609
2756
  if not d.name is None and d.name.startswith("MeshCore-"):
2610
2757
  print(f" {d.address} {d.name}")
2611
2758
  except BleakError:
2612
- print(" No BLE HW")
2759
+ print(" No BLE HW")
2613
2760
  print("\nSerial ports:")
2614
2761
  ports = serial.tools.list_ports.comports()
2615
2762
  for port, desc, hwid in sorted(ports):
@@ -2650,7 +2797,7 @@ async def main(argv):
2650
2797
  else:
2651
2798
  logger.error("Invalid choice")
2652
2799
  return
2653
-
2800
+
2654
2801
  if (debug==True):
2655
2802
  logger.setLevel(logging.DEBUG)
2656
2803
  elif (json_output) :
@@ -2668,7 +2815,10 @@ async def main(argv):
2668
2815
  elif address and len(address) == 36 and len(address.split("-")) == 5:
2669
2816
  client = BleakClient(address) # mac uses uuid, we'll pass a client
2670
2817
  else:
2671
- 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}")
2672
2822
  devices = await BleakScanner.discover(timeout=timeout)
2673
2823
  found = False
2674
2824
  for d in devices:
@@ -2693,6 +2843,26 @@ async def main(argv):
2693
2843
  mc = await MeshCore.create_ble(address=address, device=device, client=client, debug=debug, only_error=json_output, pin=pin)
2694
2844
  except ConnectionError :
2695
2845
  logger.info("Error while connecting, retrying once ...")
2846
+ if device is None and 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
2696
2866
  try :
2697
2867
  mc = await MeshCore.create_ble(address=address, device=device, client=client, debug=debug, only_error=json_output, pin=pin)
2698
2868
  except ConnectionError :
File without changes
File without changes
File without changes
File without changes
File without changes