meshcore-cli 1.1.40__py3-none-any.whl → 1.2.10__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,29 +1,39 @@
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
- import os, sys
7
+ import os, sys, io, platform
7
8
  import time, datetime
8
9
  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
+ from hashlib import sha256
28
+ from Crypto.Cipher import AES
29
+ from Crypto.Hash import HMAC, SHA256
30
+
31
+ import re
22
32
 
23
33
  from meshcore import MeshCore, EventType, logger
24
34
 
25
35
  # Version
26
- VERSION = "v1.1.40"
36
+ VERSION = "v1.2.10"
27
37
 
28
38
  # default ble address is stored in a config file
29
39
  MCCLI_CONFIG_DIR = str(Path.home()) + "/.config/meshcore/"
@@ -45,6 +55,7 @@ ANSI_INVERT = "\033[7m"
45
55
  ANSI_NORMAL = "\033[27m"
46
56
  ANSI_GREEN = "\033[0;32m"
47
57
  ANSI_BGREEN = "\033[1;32m"
58
+ ANSI_DGREEN="\033[0;38;5;22m"
48
59
  ANSI_BLUE = "\033[0;34m"
49
60
  ANSI_BBLUE = "\033[1;34m"
50
61
  ANSI_RED = "\033[0;31m"
@@ -65,6 +76,15 @@ ANSI_BORANGE="\033[1;38;5;214m"
65
76
  ANSI_YELLOW = "\033[0;33m"
66
77
  ANSI_BYELLOW = "\033[1;33m"
67
78
 
79
+ #Unicode chars
80
+ # some possible symbols for prompts 🭬🬛🬗🭬🬛🬃🬗🭬🬛🬃🬗🬏🭀🭋🭨🮋
81
+ ARROW_TAIL = "🭨"
82
+ ARROW_HEAD = "🭬"
83
+
84
+ if platform.system() == 'Windows' or platform.system() == 'Darwin':
85
+ ARROW_TAIL = ""
86
+ ARROW_HEAD = " "
87
+
68
88
  def escape_ansi(line):
69
89
  ansi_escape = re.compile(r'(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]')
70
90
  return ansi_escape.sub('', line)
@@ -186,6 +206,66 @@ process_event_message.print_snr=False
186
206
  process_event_message.color=True
187
207
  process_event_message.last_node=None
188
208
 
209
+ async def handle_log_rx(event):
210
+ mc = handle_log_rx.mc
211
+ if handle_log_rx.json_log_rx: # json mode ... raw dump
212
+ msg = json.dumps(event.payload)
213
+ if handle_message.above:
214
+ print_above(msg)
215
+ else :
216
+ print(msg)
217
+ return
218
+
219
+ pkt = bytes().fromhex(event.payload["payload"])
220
+ pbuf = io.BytesIO(pkt)
221
+ header = pbuf.read(1)[0]
222
+
223
+ if header & ~1 == 0x14: # flood msg / channel
224
+ if handle_log_rx.channel_echoes:
225
+ if header & 1 == 0: # has transport code
226
+ pbuf.read(4) # discard transport code
227
+ path_len = pbuf.read(1)[0]
228
+ path = pbuf.read(path_len).hex()
229
+ chan_hash = pbuf.read(1).hex()
230
+ cipher_mac = pbuf.read(2)
231
+ msg = pbuf.read() # until the end of buffer
232
+
233
+ channel = None
234
+ for c in await get_channels(mc):
235
+ if c["channel_hash"] == chan_hash : # validate against MAC
236
+ h = HMAC.new(bytes.fromhex(c["channel_secret"]), digestmod=SHA256)
237
+ h.update(msg)
238
+ if h.digest()[0:2] == cipher_mac:
239
+ channel = c
240
+ break
241
+
242
+ chan_name = ""
243
+
244
+ if channel is None :
245
+ if handle_log_rx.echo_unk_chans:
246
+ chan_name = chan_hash
247
+ message = msg.hex()
248
+ else:
249
+ chan_name = channel["channel_name"]
250
+ aes_key = bytes.fromhex(channel["channel_secret"])
251
+ cipher = AES.new(aes_key, AES.MODE_ECB)
252
+ message = cipher.decrypt(msg)[5:].decode("utf-8").strip("\x00")
253
+
254
+ if chan_name != "" :
255
+ width = os.get_terminal_size().columns
256
+ cars = width - 13 - 2 * path_len - len(chan_name) - 1
257
+ dispmsg = message[0:cars]
258
+ txt = f"{ANSI_LIGHT_GRAY}{chan_name} {ANSI_DGREEN}{dispmsg+(cars-len(dispmsg))*' '} {ANSI_YELLOW}[{path}]{ANSI_LIGHT_GRAY}{event.payload['snr']:6,.2f}{event.payload['rssi']:4}{ANSI_END}"
259
+ if handle_message.above:
260
+ print_above(txt)
261
+ else:
262
+ print(txt)
263
+
264
+ handle_log_rx.json_log_rx = False
265
+ handle_log_rx.channel_echoes = False
266
+ handle_log_rx.mc = None
267
+ handle_log_rx.echo_unk_chans=False
268
+
189
269
  async def handle_advert(event):
190
270
  if not handle_advert.print_adverts:
191
271
  return
@@ -257,7 +337,7 @@ async def log_message(mc, msg):
257
337
  if msg["type"] == "PRIV" :
258
338
  ct = mc.get_contact_by_key_prefix(msg['pubkey_prefix'])
259
339
  if ct is None:
260
- msg["name"] = data["pubkey_prefix"]
340
+ msg["name"] = msg["pubkey_prefix"]
261
341
  else:
262
342
  msg["name"] = ct["adv_name"]
263
343
  elif msg["type"] == "CHAN" :
@@ -296,6 +376,29 @@ async def subscribe_to_msgs(mc, json_output=False, above=False):
296
376
  CS = mc.subscribe(EventType.CHANNEL_MSG_RECV, handle_message)
297
377
  await mc.start_auto_message_fetching()
298
378
 
379
+ # redefine get_completion to let user put symbols in first item
380
+ # and handle navigating in path ...
381
+ class MyNestedCompleter(NestedCompleter):
382
+ def get_completions( self, document, complete_event):
383
+ txt = document.text_before_cursor.lstrip()
384
+ if not " " in txt:
385
+ if txt != "" and txt[0] == "/" and txt.count("/") == 1:
386
+ opts = []
387
+ for k in self.options.keys():
388
+ if k[0] == "/" :
389
+ v = "/" + k.split("/")[1] #+ ("/" if k.count("/") == 2 else "")
390
+ if v not in opts:
391
+ opts.append(v)
392
+ else:
393
+ opts = self.options.keys()
394
+ completer = WordCompleter(
395
+ opts, ignore_case=self.ignore_case,
396
+ pattern=re.compile(r"([a-zA-Z0-9_\\/\#]+|[^a-zA-Z0-9_\s\#]+)"))
397
+ yield from completer.get_completions(document, complete_event)
398
+ else: # normal behavior for remainder
399
+ yield from super().get_completions(document, complete_event)
400
+
401
+
299
402
  def make_completion_dict(contacts, pending={}, to=None, channels=None):
300
403
  contact_list = {}
301
404
  pending_list = {}
@@ -306,12 +409,15 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
306
409
  if not process_event_message.last_node is None:
307
410
  to_list["!"] = None
308
411
  to_list[".."] = None
309
- to_list["public"] = None
310
412
 
311
413
  it = iter(contacts.items())
312
414
  for c in it :
313
415
  contact_list[c[1]['adv_name']] = None
314
416
 
417
+ pit = iter(pending.items())
418
+ for c in pit :
419
+ pending_list[c[1]['adv_name']] = None
420
+
315
421
  pit = iter(pending.items())
316
422
  for c in pit :
317
423
  pending_list[c[1]['public_key']] = None
@@ -332,229 +438,269 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
332
438
  "chan" : None,
333
439
  }
334
440
 
441
+ root_completion_list = {
442
+ "ver" : None,
443
+ "infos" : None,
444
+ "advert" : None,
445
+ "floodadv" : None,
446
+ "msg" : contact_list,
447
+ "wait_ack" : None,
448
+ "time" : None,
449
+ "clock" : {"sync" : None},
450
+ "reboot" : None,
451
+ "card" : None,
452
+ "upload_card" : None,
453
+ "contacts": None,
454
+ "pending_contacts": None,
455
+ "add_pending": pending_list,
456
+ "flush_pending": None,
457
+ "contact_info": contact_list,
458
+ "export_contact" : contact_list,
459
+ "upload_contact" : contact_list,
460
+ "share_contact" : contact_list,
461
+ "path": contact_list,
462
+ "disc_path" : contact_list,
463
+ "node_discover": {"all":None, "sens":None, "rep":None, "comp":None, "room":None, "cli":None},
464
+ "trace" : None,
465
+ "reset_path" : contact_list,
466
+ "change_path" : contact_list,
467
+ "change_flags" : contact_list,
468
+ "remove_contact" : contact_list,
469
+ "import_contact" : {"meshcore://":None},
470
+ "reload_contacts" : None,
471
+ "login" : contact_list,
472
+ "cmd" : contact_list,
473
+ "req_status" : contact_list,
474
+ "req_bstatus" : contact_list,
475
+ "logout" : contact_list,
476
+ "req_telemetry" : contact_list,
477
+ "req_binary" : contact_list,
478
+ "req_mma" : contact_list,
479
+ "self_telemetry" : None,
480
+ "get_channel": None,
481
+ "set_channel": None,
482
+ "get_channels": None,
483
+ "remove_channel": None,
484
+ "apply_to": None,
485
+ "at": None,
486
+ "scope": None,
487
+ "set" : {
488
+ "name" : None,
489
+ "pin" : None,
490
+ "radio" : {",,,":None, "f,bw,sf,cr":None},
491
+ "tx" : None,
492
+ "tuning" : {",", "af,tx_d"},
493
+ "lat" : None,
494
+ "lon" : None,
495
+ "coords" : None,
496
+ "print_snr" : {"on":None, "off": None},
497
+ "json_msgs" : {"on":None, "off": None},
498
+ "color" : {"on":None, "off":None},
499
+ "print_name" : {"on":None, "off":None},
500
+ "print_adverts" : {"on":None, "off":None},
501
+ "json_log_rx" : {"on":None, "off":None},
502
+ "channel_echoes" : {"on":None, "off":None},
503
+ "echo_unk_chans" : {"on":None, "off":None},
504
+ "print_new_contacts" : {"on": None, "off":None},
505
+ "print_path_updates" : {"on":None,"off":None},
506
+ "classic_prompt" : {"on" : None, "off":None},
507
+ "manual_add_contacts" : {"on" : None, "off":None},
508
+ "telemetry_mode_base" : {"always" : None, "device":None, "never":None},
509
+ "telemetry_mode_loc" : {"always" : None, "device":None, "never":None},
510
+ "telemetry_mode_env" : {"always" : None, "device":None, "never":None},
511
+ "advert_loc_policy" : {"none" : None, "share" : None},
512
+ "auto_update_contacts" : {"on":None, "off":None},
513
+ "multi_acks" : {"on": None, "off":None},
514
+ "max_attempts" : None,
515
+ "max_flood_attempts" : None,
516
+ "flood_after" : None,
517
+ },
518
+ "get" : {"name":None,
519
+ "bat":None,
520
+ "fstats": None,
521
+ "radio":None,
522
+ "tx":None,
523
+ "coords":None,
524
+ "lat":None,
525
+ "lon":None,
526
+ "print_snr":None,
527
+ "json_msgs":None,
528
+ "color":None,
529
+ "print_name":None,
530
+ "print_adverts":None,
531
+ "json_log_rx":None,
532
+ "channel_echoes":None,
533
+ "echo_unk_chans":None,
534
+ "print_path_updates":None,
535
+ "print_new_contacts":None,
536
+ "classic_prompt":None,
537
+ "manual_add_contacts":None,
538
+ "telemetry_mode_base":None,
539
+ "telemetry_mode_loc":None,
540
+ "telemetry_mode_env":None,
541
+ "advert_loc_policy":None,
542
+ "auto_update_contacts":None,
543
+ "multi_acks":None,
544
+ "max_attempts":None,
545
+ "max_flood_attempts":None,
546
+ "flood_after":None,
547
+ "custom":None,
548
+ },
549
+ }
550
+
551
+ contact_completion_list = {
552
+ "contact_info": None,
553
+ "contact_name": None,
554
+ "contact_lastmod": None,
555
+ "export_contact" : None,
556
+ "share_contact" : None,
557
+ "upload_contact" : None,
558
+ "path": None,
559
+ "disc_path": None,
560
+ "trace": None,
561
+ "dtrace": None,
562
+ "reset_path" : None,
563
+ "change_path" : None,
564
+ "change_flags" : None,
565
+ "req_telemetry" : None,
566
+ "req_binary" : None,
567
+ "forget_password" : None,
568
+ }
569
+
570
+ client_completion_list = dict(contact_completion_list)
571
+ client_completion_list.update({
572
+ "get" : { "timeout":None, },
573
+ "set" : { "timeout":None, },
574
+ })
575
+
576
+ repeater_completion_list = dict(contact_completion_list)
577
+ repeater_completion_list.update({
578
+ "login" : None,
579
+ "logout" : None,
580
+ "req_status" : None,
581
+ "req_bstatus" : None,
582
+ "cmd" : None,
583
+ "ver" : None,
584
+ "advert" : None,
585
+ "time" : None,
586
+ "clock" : {"sync" : None},
587
+ "reboot" : None,
588
+ "start ota" : None,
589
+ "password" : None,
590
+ "neighbors" : None,
591
+ "req_acl":None,
592
+ "setperm":contact_list,
593
+ "region" : {"get":None, "allowf": None, "denyf": None, "put": None, "remove": None, "save": None, "home": None},
594
+ "gps" : {"on":None,"off":None,"sync":None,"setloc":None,
595
+ "advert" : {"none": None, "share": None, "prefs": None},
596
+ },
597
+ "sensor": {"list": None, "set": {"gps": None}, "get": {"gps": None}},
598
+ "get" : {"name" : None,
599
+ "role":None,
600
+ "radio" : None,
601
+ "freq":None,
602
+ "tx":None,
603
+ "af" : None,
604
+ "repeat" : None,
605
+ "allow.read.only" : None,
606
+ "flood.advert.interval" : None,
607
+ "flood.max":None,
608
+ "advert.interval" : None,
609
+ "guest.password" : None,
610
+ "rxdelay": None,
611
+ "txdelay": None,
612
+ "direct.tx_delay":None,
613
+ "public.key":None,
614
+ "lat" : None,
615
+ "lon" : None,
616
+ "telemetry" : None,
617
+ "status" : None,
618
+ "timeout" : None,
619
+ "acl":None,
620
+ "bridge.enabled":None,
621
+ "bridge.delay":None,
622
+ "bridge.source":None,
623
+ "bridge.baud":None,
624
+ "bridge.secret":None,
625
+ "bridge.type":None,
626
+ },
627
+ "set" : {"name" : None,
628
+ "radio" : {",,,":None, "f,bw,sf,cr": None},
629
+ "freq" : None,
630
+ "tx" : None,
631
+ "af": None,
632
+ "repeat" : {"on": None, "off": None},
633
+ "flood.advert.interval" : None,
634
+ "flood.max" : None,
635
+ "advert.interval" : None,
636
+ "guest.password" : None,
637
+ "allow.read.only" : {"on": None, "off": None},
638
+ "rxdelay" : None,
639
+ "txdelay": None,
640
+ "direct.txdelay" : None,
641
+ "lat" : None,
642
+ "lon" : None,
643
+ "timeout" : None,
644
+ "perm":contact_list,
645
+ "bridge.enabled":{"on": None, "off": None},
646
+ "bridge.delay":None,
647
+ "bridge.source":None,
648
+ "bridge.baud":None,
649
+ "bridge.secret":None,
650
+ },
651
+ "erase": None,
652
+ "log" : {"start" : None, "stop" : None, "erase" : None}
653
+ })
654
+
655
+ sensor_completion_list = dict(repeater_completion_list)
656
+ sensor_completion_list.update({"req_mma":{"begin end":None}})
657
+ sensor_completion_list["get"].update({ "mma":None, })
658
+
335
659
  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
- })
660
+ completion_list.update(dict(root_completion_list))
435
661
  completion_list["set"].update(make_completion_dict.custom_vars)
436
662
  completion_list["get"].update(make_completion_dict.custom_vars)
437
663
  else :
438
664
  completion_list.update({
439
665
  "send" : None,
440
666
  })
667
+ if to['type'] == 1 :
668
+ completion_list.update(client_completion_list)
669
+ if to['type'] == 2 or to['type'] == 3 : # repeaters and room servers
670
+ completion_list.update(repeater_completion_list)
671
+ if (to['type'] == 4) : #specific to sensors
672
+ completion_list.update(sensor_completion_list)
441
673
 
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
- })
674
+ slash_root_completion_list = {}
675
+ for k,v in root_completion_list.items():
676
+ slash_root_completion_list["/"+k]=v
458
677
 
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
- })
678
+ completion_list.update(slash_root_completion_list)
546
679
 
547
- if (to['type'] == 4) : #specific to sensors
548
- completion_list.update({
549
- "req_mma":{"begin end":None},
550
- })
680
+ slash_contacts_completion_list = {}
681
+ for k,v in contacts.items():
682
+ d={}
683
+ if v["type"] == 1:
684
+ l = client_completion_list
685
+ elif v["type"] == 2 or v["type"] == 3:
686
+ l = repeater_completion_list
687
+ elif v["type"] == 4:
688
+ l = sensor_completion_list
689
+
690
+ for kk, vv in l.items():
691
+ d["/" + v["adv_name"] + "/" + kk] = vv
551
692
 
552
- completion_list["get"].update({
553
- "mma":None,
554
- })
693
+ slash_contacts_completion_list.update(d)
555
694
 
556
- completion_list["set"].update({
557
- })
695
+ completion_list.update(slash_contacts_completion_list)
696
+
697
+ slash_chan_completion_list = {}
698
+ if not channels is None:
699
+ for c in channels :
700
+ if c["channel_name"] != "":
701
+ slash_chan_completion_list["/" + c["channel_name"]] = None
702
+
703
+ completion_list.update(slash_chan_completion_list)
558
704
 
559
705
  completion_list.update({
560
706
  "script" : None,
@@ -573,7 +719,8 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
573
719
  contact = to
574
720
  prev_contact = None
575
721
 
576
- # await get_contacts(mc, anim=True)
722
+ scope = await set_scope(mc, "*")
723
+
577
724
  await get_contacts(mc, anim=True)
578
725
  await get_channels(mc, anim=True)
579
726
  await subscribe_to_msgs(mc, above=True)
@@ -612,6 +759,9 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
612
759
 
613
760
  last_ack = True
614
761
  while True:
762
+ # reset scope (if changed)
763
+ scope = await set_scope(mc, scope)
764
+
615
765
  color = process_event_message.color
616
766
  classic = interactive_loop.classic or not color
617
767
  print_name = interactive_loop.print_name
@@ -621,14 +771,16 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
621
771
  else:
622
772
  prompt = f"{ANSI_INVERT}"
623
773
 
624
- # some possible symbols for prompts 🭬🬛🬗🭬🬛🬃🬗🭬🬛🬃🬗🬏🭀🭋🭨🮋
625
774
  if print_name or contact is None :
626
775
  prompt = prompt + f"{ANSI_BGRAY}"
627
776
  prompt = prompt + f"{mc.self_info['name']}"
777
+ if contact is None: # display scope
778
+ if not scope is None:
779
+ prompt = prompt + f"|{scope}"
628
780
  if classic :
629
781
  prompt = prompt + " > "
630
782
  else :
631
- prompt = prompt + "🭨"
783
+ prompt = prompt + f"{ANSI_NORMAL}{ARROW_HEAD}{ANSI_INVERT}"
632
784
 
633
785
  if not contact is None :
634
786
  if not last_ack:
@@ -649,13 +801,24 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
649
801
  prompt = prompt + f"{ANSI_INVERT}"
650
802
 
651
803
  if print_name and not classic :
652
- prompt = prompt + "🭬"
804
+ prompt = prompt + f"{ANSI_NORMAL}{ARROW_TAIL}{ANSI_INVERT}"
653
805
 
654
806
  prompt = prompt + f"{contact['adv_name']}"
807
+ if contact["type"] == 0 or contact["out_path_len"]==-1:
808
+ if scope is None:
809
+ prompt = prompt + f"|*"
810
+ else:
811
+ prompt = prompt + f"|{scope}"
812
+ else: # display path to dest or 0 if 0 hop
813
+ if contact["out_path_len"] == 0:
814
+ prompt = prompt + f"|0"
815
+ else:
816
+ prompt = prompt + "|" + contact["out_path"]
817
+
655
818
  if classic :
656
819
  prompt = prompt + f"{ANSI_NORMAL} > "
657
820
  else:
658
- prompt = prompt + f"{ANSI_NORMAL}🭬"
821
+ prompt = prompt + f"{ANSI_NORMAL}{ARROW_HEAD}"
659
822
 
660
823
  prompt = prompt + f"{ANSI_END}"
661
824
 
@@ -665,7 +828,7 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
665
828
  session.app.ttimeoutlen = 0.2
666
829
  session.app.timeoutlen = 0.2
667
830
 
668
- completer = NestedCompleter.from_nested_dict(
831
+ completer = MyNestedCompleter.from_nested_dict(
669
832
  make_completion_dict(mc.contacts,
670
833
  mc.pending_contacts,
671
834
  to=contact,
@@ -676,18 +839,91 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
676
839
  completer=completer,
677
840
  key_bindings=bindings)
678
841
 
842
+ line = line.strip()
843
+
679
844
  if line == "" : # blank line
680
845
  pass
681
846
 
847
+ elif line.startswith("?") :
848
+ get_help_for(line[1:], context="chat")
849
+
682
850
  # raw meshcli command as on command line
683
851
  elif line.startswith("$") :
684
- args = shlex.split(line[1:])
685
- await process_cmds(mc, args)
852
+ try :
853
+ args = shlex.split(line[1:])
854
+ await process_cmds(mc, args)
855
+ except ValueError:
856
+ logger.error("Error parsing line {line[1:]}")
857
+
858
+ elif line.startswith("/scope") or\
859
+ line.startswith("scope") and contact is None:
860
+ if not scope is None:
861
+ prev_scope = scope
862
+ try:
863
+ newscope = line.split(" ", 1)[1]
864
+ scope = await set_scope(mc, newscope)
865
+ except IndexError:
866
+ print(scope)
867
+
868
+ elif contact is None and (line.startswith("apply_to ") or line.startswith("at ")) or\
869
+ line.startswith("/apply_to ") or line.startswith("/at ") :
870
+ try:
871
+ await apply_command_to_contacts(mc, line.split(" ",2)[1], line.split(" ",2)[2])
872
+ except IndexError:
873
+ logger.error(f"Error with apply_to command parameters")
874
+
875
+ elif line.startswith("/") :
876
+ path = line.split(" ", 1)[0]
877
+ if path.count("/") == 1:
878
+ args = line[1:].split(" ")
879
+ dest = args[0]
880
+ dest_scope = None
881
+ if "%" in dest :
882
+ dest_scope = dest.split("%")[-1]
883
+ dest = dest[:-len(dest_scope)-1]
884
+ await set_scope (mc, dest_scope)
885
+ tct = mc.get_contact_by_name(dest)
886
+ if len(args)>1 and not tct is None: # a contact, send a message
887
+ if tct["type"] == 1 or tct["type"] == 3: # client or room
888
+ last_ack = await msg_ack(mc, tct, line.split(" ", 1)[1])
889
+ else:
890
+ print("Can only send msg to chan, client or room")
891
+ else :
892
+ ch = await get_channel_by_name(mc, dest)
893
+ if len(args)>1 and not ch is None: # a channel, send message
894
+ await send_chan_msg(mc, ch["channel_idx"], line.split(" ", 1)[1])
895
+ else :
896
+ try :
897
+ await process_cmds(mc, shlex.split(line[1:]))
898
+ except ValueError:
899
+ logger.error(f"Error processing line{line[1:]}")
900
+ else:
901
+ cmdline = line[1:].split("/",1)[1]
902
+ contact_name = path[1:].split("/",1)[0]
903
+ dest_scope = None
904
+ if "%" in contact_name:
905
+ dest_scope = contact_name.split("%")[-1]
906
+ contact_name = contact_name[:-len(dest_scope)-1]
907
+ await set_scope (mc, dest_scope)
908
+ tct = mc.get_contact_by_name(contact_name)
909
+ if tct is None:
910
+ print(f"{contact_name} is not a contact")
911
+ else:
912
+ if not await process_contact_chat_line(mc, tct, cmdline):
913
+ if cmdline != "":
914
+ if tct["type"] == 1:
915
+ last_ack = await msg_ack(mc, tct, cmdline)
916
+ else :
917
+ await process_cmds(mc, ["cmd", tct["adv_name"], cmdline])
686
918
 
687
919
  elif line.startswith("to ") : # dest
688
920
  dest = line[3:]
689
921
  if dest.startswith("\"") or dest.startswith("\'") : # if name starts with a quote
690
922
  dest = shlex.split(dest)[0] # use shlex.split to get contact name between quotes
923
+ dest_scope = None
924
+ if '%' in dest and scope!=None :
925
+ dest_scope = dest.split("%")[-1]
926
+ dest = dest[:-len(dest_scope)-1]
691
927
  nc = mc.get_contact_by_name(dest)
692
928
  if nc is None:
693
929
  if dest == "public" :
@@ -701,12 +937,14 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
701
937
  nc["adv_name"] = mc.channels[dest]["channel_name"]
702
938
  elif dest == ".." : # previous recipient
703
939
  nc = prev_contact
940
+ if dest_scope is None and not scope is None:
941
+ dest_scope = prev_scope
704
942
  elif dest == "~" or dest == "/" or dest == mc.self_info['name']:
705
943
  nc = None
706
944
  elif dest == "!" :
707
945
  nc = process_event_message.last_node
708
946
  else :
709
- chan = await get_channel_by_name(mc, dest)
947
+ chan = await get_channel_by_name(mc, dest)
710
948
  if chan is None :
711
949
  print(f"Contact '{dest}' not found in contacts.")
712
950
  nc = contact
@@ -718,6 +956,12 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
718
956
  last_ack = True
719
957
  prev_contact = contact
720
958
  contact = nc
959
+ if dest_scope is None:
960
+ dest_scope = scope
961
+ if not scope is None and dest_scope != scope:
962
+ prev_scope = scope
963
+ if not dest_scope is None:
964
+ scope = await set_scope(mc, dest_scope)
721
965
 
722
966
  elif line == "to" :
723
967
  if contact is None :
@@ -740,7 +984,7 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
740
984
  if ln is None :
741
985
  print("No received msg yet !")
742
986
  elif ln["type"] == 0 :
743
- await process_cmds(mc, ["chan", str(contact["chan_nb"]), line] )
987
+ await send_chan_msg(mc, ln["chan_nb"], line[1:])
744
988
  else :
745
989
  last_ack = await msg_ack(mc, ln, line[1:])
746
990
  if last_ack == False :
@@ -748,107 +992,14 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
748
992
 
749
993
  # commands are passed through if at root
750
994
  elif contact is None or line.startswith(".") :
751
- args = shlex.split(line)
752
- await process_cmds(mc, args)
753
-
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
995
  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)
996
+ args = shlex.split(line)
997
+ await process_cmds(mc, args)
998
+ except ValueError:
999
+ logger.error(f"Error processing {line}")
844
1000
 
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)
1001
+ elif await process_contact_chat_line(mc, contact, line):
1002
+ pass
852
1003
 
853
1004
  elif line == "list" : # list command from chat displays contacts on a line
854
1005
  it = iter(mc.contacts.items())
@@ -868,7 +1019,7 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
868
1019
  last_ack = await msg_ack(mc, contact, line)
869
1020
 
870
1021
  elif contact["type"] == 0 : # channel, send msg to channel
871
- await process_cmds(mc, ["chan", str(contact["chan_nb"]), line] )
1022
+ await send_chan_msg(mc, contact["chan_nb"], line)
872
1023
 
873
1024
  elif contact["type"] == 1 : # chat, send to recipient and wait ack
874
1025
  last_ack = await msg_ack(mc, contact, line)
@@ -886,6 +1037,291 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
886
1037
  interactive_loop.classic = False
887
1038
  interactive_loop.print_name = True
888
1039
 
1040
+ async def process_contact_chat_line(mc, contact, line):
1041
+ if contact["type"] == 0:
1042
+ return False
1043
+
1044
+ # if one element in line (most cases) strip the scope and apply it
1045
+ if not " " in line and "%" in line:
1046
+ dest_scope = line.split("%")[-1]
1047
+ line = line[:-len(dest_scope)-1]
1048
+ await set_scope (mc, dest_scope)
1049
+
1050
+ if line.startswith(":") : # : will send a command to current recipient
1051
+ args=["cmd", contact['adv_name'], line[1:]]
1052
+ await process_cmds(mc, args)
1053
+ return True
1054
+
1055
+ if line == "reset path" : # reset path for compat with terminal chat
1056
+ args = ["reset_path", contact['adv_name']]
1057
+ await process_cmds(mc, args)
1058
+ return True
1059
+
1060
+ if line.startswith("contact_name") or line.startswith("cn"):
1061
+ print(contact['adv_name'],end="")
1062
+ if " " in line:
1063
+ print(" ", end="", flush=True)
1064
+ secline = line.split(" ", 1)[1]
1065
+ await process_contact_chat_line(mc, contact, secline)
1066
+ else:
1067
+ print("")
1068
+ return True
1069
+
1070
+ if line == "contact_lastmod":
1071
+ timestamp = contact["lastmod"]
1072
+ print(f"{contact['adv_name']} updated"
1073
+ f" {datetime.datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d at %H:%M:%S')}"
1074
+ f" ({timestamp})")
1075
+ return True
1076
+
1077
+ # commands that take contact as second arg will be sent to recipient
1078
+ if line == "sc" or line == "share_contact" or\
1079
+ line == "ec" or line == "export_contact" or\
1080
+ line == "uc" or line == "upload_contact" or\
1081
+ line == "rp" or line == "reset_path" or\
1082
+ line == "dp" or line == "disc_path" or\
1083
+ line == "contact_info" or line == "ci" or\
1084
+ line == "req_status" or line == "rs" or\
1085
+ line == "req_bstatus" or line == "rbs" or\
1086
+ line == "req_telemetry" or line == "rt" or\
1087
+ line == "req_acl" or\
1088
+ line == "path" or\
1089
+ line == "logout" :
1090
+ args = [line, contact['adv_name']]
1091
+ await process_cmds(mc, args)
1092
+ return True
1093
+
1094
+ # special case for rp that can be chained from cmdline
1095
+ if line.startswith("rp ") or line.startswith("reset_path ") :
1096
+ args = ["rp", contact['adv_name']]
1097
+ await process_cmds(mc, args)
1098
+ secline = line.split(" ", 1)[1]
1099
+ await process_contact_chat_line(mc, contact, secline)
1100
+ return True
1101
+
1102
+ if line.startswith("set timeout "):
1103
+ cmds=line.split(" ")
1104
+ contact["timeout"] = float(cmds[2])
1105
+ return True
1106
+
1107
+ if line == "get timeout":
1108
+ print(f"timeout: {0 if not 'timeout' in contact else contact['timeout']}")
1109
+ return True
1110
+
1111
+ if contact["type"] == 4 and\
1112
+ (line.startswith("get mma ")) or\
1113
+ contact["type"] > 1 and\
1114
+ (line.startswith("get telemetry") or line.startswith("get status") or line.startswith("get acl")):
1115
+ cmds = line.split(" ")
1116
+ args = [f"req_{cmds[1]}", contact['adv_name']]
1117
+ if len(cmds) > 2 :
1118
+ args = args + cmds[2:]
1119
+ if line.startswith("get mma ") and len(args) < 4:
1120
+ args.append("0")
1121
+ await process_cmds(mc, args)
1122
+ return True
1123
+
1124
+ # special treatment for setperm to support contact name as param
1125
+ if contact["type"] > 1 and\
1126
+ (line.startswith("setperm ") or line.startswith("set perm ")):
1127
+ try:
1128
+ cmds = shlex.split(line)
1129
+ off = 1 if line.startswith("set perm") else 0
1130
+ name = cmds[1 + off]
1131
+ perm_string = cmds[2 + off]
1132
+ if (perm_string.startswith("0x")):
1133
+ perm = int(perm_string,0)
1134
+ elif (perm_string.startswith("#")):
1135
+ perm = int(perm_string[1:])
1136
+ else:
1137
+ perm = int(perm_string,16)
1138
+ ct=mc.get_contact_by_name(name)
1139
+ if ct is None:
1140
+ ct=mc.get_contact_by_key_prefix(name)
1141
+ if ct is None:
1142
+ if name == "self" or mc.self_info["public_key"].startswith(name):
1143
+ key = mc.self_info["public_key"]
1144
+ else:
1145
+ key = name
1146
+ else:
1147
+ key=ct["public_key"]
1148
+ newline=f"setperm {key} {perm}"
1149
+ await process_cmds(mc, ["cmd", contact["adv_name"], newline])
1150
+ except IndexError:
1151
+ print("Wrong number of parameters")
1152
+ return True
1153
+
1154
+ # trace called on a contact
1155
+ if line == "trace" or line == "tr" :
1156
+ await print_trace_to(mc, contact)
1157
+ return True
1158
+
1159
+ if line == "dtrace" or line == "dt" :
1160
+ await print_disc_trace_to(mc, contact)
1161
+ return True
1162
+
1163
+ # same but for commands with a parameter
1164
+ if " " in line:
1165
+ cmds = line.split(" ", 1)
1166
+ if "%" in cmds[0]:
1167
+ dest_scope = cmds[0].split("%")[-1]
1168
+ cmds[0] = cmds[0][:-len(dest_scope)-1]
1169
+ await set_scope(mc, dest_scope)
1170
+
1171
+ if cmds[0] == "cmd" or cmds[0] == "msg" or\
1172
+ cmds[0] == "cp" or cmds[0] == "change_path" or\
1173
+ cmds[0] == "cf" or cmds[0] == "change_flags" or\
1174
+ cmds[0] == "req_binary" or\
1175
+ cmds[0] == "login" :
1176
+ args = [cmds[0], contact['adv_name'], cmds[1]]
1177
+ await process_cmds(mc, args)
1178
+ return True
1179
+
1180
+ if line == "login": # use stored password or prompt for it
1181
+ password_file = ""
1182
+ password = ""
1183
+ if os.path.isdir(MCCLI_CONFIG_DIR) :
1184
+ # if a password file exists with node name open it and destroy it
1185
+ password_file = MCCLI_CONFIG_DIR + contact['adv_name'] + ".pass"
1186
+ if os.path.exists(password_file) :
1187
+ with open(password_file, "r", encoding="utf-8") as f :
1188
+ password=f.readline().strip()
1189
+ os.remove(password_file)
1190
+ password_file = MCCLI_CONFIG_DIR + contact["public_key"] + ".pass"
1191
+ with open(password_file, "w", encoding="utf-8") as f :
1192
+ f.write(password)
1193
+
1194
+ # this is the new correct password file, using pubkey
1195
+ password_file = MCCLI_CONFIG_DIR + contact["public_key"] + ".pass"
1196
+ if os.path.exists(password_file) :
1197
+ with open(password_file, "r", encoding="utf-8") as f :
1198
+ password=f.readline().strip()
1199
+
1200
+ if password == "":
1201
+ try:
1202
+ sess = PromptSession(f"Password for {contact['adv_name']}: ", is_password=True)
1203
+ password = await sess.prompt_async()
1204
+ except EOFError:
1205
+ logger.info("Canceled")
1206
+ return True
1207
+
1208
+ if password_file != "":
1209
+ with open(password_file, "w", encoding="utf-8") as f :
1210
+ f.write(password)
1211
+
1212
+ args = ["login", contact['adv_name'], password]
1213
+ await process_cmds(mc, args)
1214
+ return True
1215
+
1216
+ if line.startswith("forget_password") or line.startswith("fp"):
1217
+ password_file = MCCLI_CONFIG_DIR + contact['adv_name'] + ".pass"
1218
+ if os.path.exists(password_file):
1219
+ os.remove(password_file)
1220
+ password_file = MCCLI_CONFIG_DIR + contact['public_key'] + ".pass"
1221
+ if os.path.exists(password_file):
1222
+ os.remove(password_file)
1223
+ try:
1224
+ secline = line.split(" ", 1)[1]
1225
+ await process_contact_chat_line(mc, contact, secline)
1226
+ except IndexError:
1227
+ pass
1228
+ return True
1229
+
1230
+ if contact["type"] == 4 and \
1231
+ (line.startswith("req_mma ") or line.startswith('rm ')) :
1232
+ cmds = line.split(" ")
1233
+ if len(cmds) < 3 :
1234
+ cmds.append("0")
1235
+ args = [cmds[0], contact['adv_name'], cmds[1], cmds[2]]
1236
+ await process_cmds(mc, args)
1237
+ return True
1238
+
1239
+ return False
1240
+
1241
+ async def apply_command_to_contacts(mc, contact_filter, line):
1242
+ upd_before = None
1243
+ upd_after = None
1244
+ contact_type = None
1245
+ min_hops = None
1246
+ max_hops = None
1247
+
1248
+ await mc.ensure_contacts()
1249
+
1250
+ filters = contact_filter.split(",")
1251
+ for f in filters:
1252
+ if f == "all":
1253
+ pass
1254
+ elif f[0] == "u": #updated
1255
+ val_str = f[2:]
1256
+ t = time.time()
1257
+ if val_str[-1] == "d": # value in days
1258
+ t = t - float(val_str[0:-1]) * 86400
1259
+ elif val_str[-1] == "h": # value in hours
1260
+ t = t - float(val_str[0:-1]) * 3600
1261
+ elif val_str[-1] == "m": # value in minutes
1262
+ t = t - float(val_str[0:-1]) * 60
1263
+ else:
1264
+ t = int(val_str)
1265
+ if f[1] == "<": #before
1266
+ upd_before = t
1267
+ elif f[1] == ">":
1268
+ upd_after = t
1269
+ else:
1270
+ logger.error(f"Time filter can only be < or >")
1271
+ return
1272
+ elif f[0] == "t": # type
1273
+ if f[1] == "=":
1274
+ contact_type = int(f[2:])
1275
+ else:
1276
+ logger.error(f"Type can only be equals to a value")
1277
+ return
1278
+ elif f[0] == "d": # direct
1279
+ min_hops=0
1280
+ elif f[0] == "f": # flood
1281
+ max_hops=-1
1282
+ elif f[0] == "h": # hop number
1283
+ if f[1] == ">":
1284
+ min_hops = int(f[2:])+1
1285
+ elif f[1] == "<":
1286
+ max_hops = int(f[2:])-1
1287
+ elif f[1] == "=":
1288
+ min_hops = int(f[2:])
1289
+ max_hops = int(f[2:])
1290
+ else:
1291
+ logger.error(f"Unknown filter {f}")
1292
+ return
1293
+
1294
+ for c in dict(mc._contacts).items():
1295
+ contact = c[1]
1296
+ if (contact_type is None or contact["type"] == contact_type) and\
1297
+ (upd_before is None or contact["lastmod"] < upd_before) and\
1298
+ (upd_after is None or contact["lastmod"] > upd_after) and\
1299
+ (min_hops is None or contact["out_path_len"] >= min_hops) and\
1300
+ (max_hops is None or contact["out_path_len"] <= max_hops):
1301
+ if await process_contact_chat_line(mc, contact, line):
1302
+ pass
1303
+
1304
+ elif line == "remove_contact":
1305
+ args = [line, contact['adv_name']]
1306
+ await process_cmds(mc, args)
1307
+
1308
+ elif line.startswith("send") or line.startswith("\"") :
1309
+ if line.startswith("send") :
1310
+ line = line[5:]
1311
+ if line.startswith("\"") :
1312
+ line = line[1:]
1313
+ await msg_ack(mc, contact, line)
1314
+
1315
+ elif contact["type"] == 2 or\
1316
+ contact["type"] == 3 or\
1317
+ contact["type"] == 4 : # repeater, room, sensor send cmd
1318
+ await process_cmds(mc, ["cmd", contact["adv_name"], line])
1319
+ # wait for a reply from cmd
1320
+ await mc.wait_for_event(EventType.MESSAGES_WAITING, timeout=7)
1321
+
1322
+ else:
1323
+ logger.error(f"Can't send {line} to {contact['adv_name']}")
1324
+
889
1325
  async def send_cmd (mc, contact, cmd) :
890
1326
  res = await mc.commands.send_cmd(contact, cmd)
891
1327
  if not res is None and not res.type == EventType.ERROR:
@@ -909,7 +1345,7 @@ async def send_chan_msg(mc, nb, msg):
909
1345
  sent["text"] = msg
910
1346
  sent["txt_type"] = 0
911
1347
  sent["name"] = mc.self_info['name']
912
- await log_message(mc, sent)
1348
+ await log_message(mc, sent)
913
1349
  return res
914
1350
 
915
1351
  async def send_msg (mc, contact, msg) :
@@ -928,7 +1364,7 @@ async def send_msg (mc, contact, msg) :
928
1364
 
929
1365
  async def msg_ack (mc, contact, msg) :
930
1366
  timeout = 0 if not 'timeout' in contact else contact['timeout']
931
- res = await mc.commands.send_msg_with_retry(contact, msg,
1367
+ res = await mc.commands.send_msg_with_retry(contact, msg,
932
1368
  max_attempts=msg_ack.max_attempts,
933
1369
  flood_after=msg_ack.flood_after,
934
1370
  max_flood_attempts=msg_ack.max_flood_attempts,
@@ -948,6 +1384,28 @@ msg_ack.max_attempts=3
948
1384
  msg_ack.flood_after=2
949
1385
  msg_ack.max_flood_attempts=1
950
1386
 
1387
+ async def set_scope (mc, scope) :
1388
+ if not set_scope.has_scope:
1389
+ return None
1390
+
1391
+ if scope == "None" or scope == "0" or scope == "clear" or scope == "":
1392
+ scope = "*"
1393
+
1394
+ if set_scope.current_scope == scope:
1395
+ return scope
1396
+
1397
+ res = await mc.commands.set_flood_scope(scope)
1398
+ if res is None or res.type == EventType.ERROR:
1399
+ if not res is None and res.payload["error_code"] == 1: #unsupported
1400
+ set_scope.has_scope = False
1401
+ return None
1402
+
1403
+ set_scope.current_scope = scope
1404
+
1405
+ return scope
1406
+ set_scope.has_scope = True
1407
+ set_scope.current_scope = None
1408
+
951
1409
  async def get_channel (mc, chan) :
952
1410
  if not chan.isnumeric():
953
1411
  return await get_channel_by_name(mc, chan)
@@ -984,6 +1442,7 @@ async def set_channel (mc, chan, name, key=None):
984
1442
  return None
985
1443
 
986
1444
  info = res.payload
1445
+ info["channel_hash"] = sha256(info["channel_secret"]).digest()[0:1].hex()
987
1446
  info["channel_secret"] = info["channel_secret"].hex()
988
1447
 
989
1448
  if hasattr(mc,'channels') :
@@ -1002,6 +1461,9 @@ async def get_channel_by_name (mc, name):
1002
1461
  return None
1003
1462
 
1004
1463
  async def get_contacts (mc, anim=False, lastomod=0, timeout=5) :
1464
+ if mc._contacts:
1465
+ return
1466
+
1005
1467
  if anim:
1006
1468
  print("Fetching contacts ", end="", flush=True)
1007
1469
 
@@ -1020,7 +1482,7 @@ async def get_contacts (mc, anim=False, lastomod=0, timeout=5) :
1020
1482
  done, pending = await asyncio.wait(
1021
1483
  futures, timeout=timeout, return_when=asyncio.FIRST_COMPLETED
1022
1484
  )
1023
-
1485
+
1024
1486
  # Check if any future completed successfully
1025
1487
  if len(done) == 0:
1026
1488
  logger.debug("Timeout while getting contacts")
@@ -1040,7 +1502,7 @@ async def get_contacts (mc, anim=False, lastomod=0, timeout=5) :
1040
1502
  if anim:
1041
1503
  if event.type == EventType.CONTACTS:
1042
1504
  print ((len(event.payload)-contact_nb)*"." + " Done")
1043
- else :
1505
+ else :
1044
1506
  print(" Error")
1045
1507
  for future in pending:
1046
1508
  future.cancel()
@@ -1069,12 +1531,14 @@ async def get_channels (mc, anim=False) :
1069
1531
  if res.type == EventType.ERROR:
1070
1532
  break
1071
1533
  info = res.payload
1534
+ info["channel_hash"] = sha256(info["channel_secret"]).digest()[0:1].hex()
1072
1535
  info["channel_secret"] = info["channel_secret"].hex()
1073
1536
  mc.channels.append(info)
1074
1537
  ch = ch + 1
1075
1538
  if anim:
1076
1539
  print(".", end="", flush=True)
1077
- print (" Done")
1540
+ if anim:
1541
+ print (" Done")
1078
1542
  return mc.channels
1079
1543
 
1080
1544
  async def print_trace_to (mc, contact):
@@ -1148,8 +1612,14 @@ async def print_disc_trace_to (mc, contact):
1148
1612
 
1149
1613
  async def next_cmd(mc, cmds, json_output=False):
1150
1614
  """ process next command """
1615
+ global ARROW_TAIL, ARROW_HEAD
1151
1616
  try :
1152
1617
  argnum = 0
1618
+
1619
+ if cmds[0].startswith("?") : # get some help
1620
+ get_help_for(cmds[0][1:], context="line")
1621
+ return cmds[argnum+1:]
1622
+
1153
1623
  if cmds[0].startswith(".") : # override json_output
1154
1624
  json_output = True
1155
1625
  cmd = cmds[0][1:]
@@ -1240,6 +1710,10 @@ async def next_cmd(mc, cmds, json_output=False):
1240
1710
  else:
1241
1711
  print("Time set")
1242
1712
 
1713
+ case "apply_to"|"at":
1714
+ argnum = 2
1715
+ await apply_command_to_contacts(mc, cmds[1], cmds[2])
1716
+
1243
1717
  case "set":
1244
1718
  argnum = 2
1245
1719
  match cmds[1]:
@@ -1272,6 +1746,10 @@ async def next_cmd(mc, cmds, json_output=False):
1272
1746
  interactive_loop.classic = (cmds[2] == "on")
1273
1747
  if json_output :
1274
1748
  print(json.dumps({"cmd" : cmds[1], "param" : cmds[2]}))
1749
+ case "arrow_tail":
1750
+ ARROW_TAIL = cmds[2]
1751
+ case "arrow_head":
1752
+ ARROW_HEAD = cmds[2]
1275
1753
  case "color" :
1276
1754
  process_event_message.color = (cmds[2] == "on")
1277
1755
  if json_output :
@@ -1280,6 +1758,18 @@ async def next_cmd(mc, cmds, json_output=False):
1280
1758
  process_event_message.print_snr = (cmds[2] == "on")
1281
1759
  if json_output :
1282
1760
  print(json.dumps({"cmd" : cmds[1], "param" : cmds[2]}))
1761
+ case "json_log_rx" :
1762
+ handle_log_rx.json_log_rx = (cmds[2] == "on")
1763
+ if json_output :
1764
+ print(json.dumps({"cmd" : cmds[1], "param" : cmds[2]}))
1765
+ case "channel_echoes" :
1766
+ handle_log_rx.channel_echoes = (cmds[2] == "on")
1767
+ if json_output :
1768
+ print(json.dumps({"cmd" : cmds[1], "param" : cmds[2]}))
1769
+ case "echo_unk_chans" :
1770
+ handle_log_rx.echo_unk_chans = (cmds[2] == "on")
1771
+ if json_output :
1772
+ print(json.dumps({"cmd" : cmds[1], "param" : cmds[2]}))
1283
1773
  case "print_adverts" :
1284
1774
  handle_advert.print_adverts = (cmds[2] == "on")
1285
1775
  if json_output :
@@ -1510,6 +2000,21 @@ async def next_cmd(mc, cmds, json_output=False):
1510
2000
  print(json.dumps({"color" : process_event_message.color}))
1511
2001
  else:
1512
2002
  print(f"{'on' if process_event_message.color else 'off'}")
2003
+ case "json_log_rx":
2004
+ if json_output :
2005
+ print(json.dumps({"json_log_rx" : handle_log_rx.json_log_rx}))
2006
+ else:
2007
+ print(f"{'on' if handle_log_rx.json_log_rx else 'off'}")
2008
+ case "channel_echoes":
2009
+ if json_output :
2010
+ print(json.dumps({"channel_echoes" : handle_log_rx.channel_echoes}))
2011
+ else:
2012
+ print(f"{'on' if handle_log_rx.channel_echoes else 'off'}")
2013
+ case "echo_unk_chans":
2014
+ if json_output :
2015
+ print(json.dumps({"echo_unk_chans" : handle_log_rx.echo_unk_chans}))
2016
+ else:
2017
+ print(f"{'on' if handle_log_rx.echo_unk_chans else 'off'}")
1513
2018
  case "print_adverts":
1514
2019
  if json_output :
1515
2020
  print(json.dumps({"print_adverts" : handle_advert.print_adverts}))
@@ -1702,11 +2207,17 @@ async def next_cmd(mc, cmds, json_output=False):
1702
2207
  res = await set_channel(mc, cmds[1], cmds[2])
1703
2208
  elif len(cmds[3]) != 32:
1704
2209
  res = None
1705
- else:
2210
+ else:
1706
2211
  res = await set_channel(mc, cmds[1], cmds[2], bytes.fromhex(cmds[3]))
1707
2212
  if res is None:
1708
2213
  print("Error setting channel")
1709
2214
 
2215
+ case "scope":
2216
+ argnum = 1
2217
+ res = await set_scope(mc, cmds[1])
2218
+ if res is None:
2219
+ print(f"Error while setting scope")
2220
+
1710
2221
  case "remove_channel":
1711
2222
  argnum = 1
1712
2223
  res = await set_channel(mc, cmds[1], "", bytes.fromhex(16*"00"))
@@ -1722,8 +2233,8 @@ async def next_cmd(mc, cmds, json_output=False):
1722
2233
  case "msg" | "m" | "{" : # sends to a contact from name
1723
2234
  argnum = 2
1724
2235
  dest = None
1725
-
1726
- if len(cmds[1]) == 12: # possibly an hex prefix
2236
+
2237
+ if len(cmds[1]) == 12: # possibly an hex prefix
1727
2238
  try:
1728
2239
  dest = bytes.fromhex(cmds[1])
1729
2240
  except ValueError:
@@ -1773,7 +2284,7 @@ async def next_cmd(mc, cmds, json_output=False):
1773
2284
  argnum = 2
1774
2285
  dest = None
1775
2286
 
1776
- if len(cmds[1]) == 12: # possibly an hex prefix
2287
+ if len(cmds[1]) == 12: # possibly an hex prefix
1777
2288
  try:
1778
2289
  dest = bytes.fromhex(cmds[1])
1779
2290
  except ValueError:
@@ -1800,11 +2311,18 @@ async def next_cmd(mc, cmds, json_output=False):
1800
2311
 
1801
2312
  case "trace" | "tr":
1802
2313
  argnum = 1
1803
- res = await mc.commands.send_trace(path=cmds[1])
2314
+ path = cmds[1]
2315
+ plen = int(len(path)/2)
2316
+ if plen > 1 and path.count(",") == 0:
2317
+ path = cmds[1][0:2]
2318
+ for i in range(1, plen):
2319
+ path = path + "," + cmds[1][2*i:2*i+2]
2320
+
2321
+ res = await mc.commands.send_trace(path=path)
1804
2322
  if res and res.type != EventType.ERROR:
1805
2323
  tag= int.from_bytes(res.payload['expected_ack'], byteorder="little")
1806
2324
  timeout = res.payload["suggested_timeout"] / 1000 * 1.2
1807
- ev = await mc.wait_for_event(EventType.TRACE_DATA,
2325
+ ev = await mc.wait_for_event(EventType.TRACE_DATA,
1808
2326
  attribute_filters={"tag": tag},
1809
2327
  timeout=timeout)
1810
2328
  if ev is None:
@@ -1839,7 +2357,7 @@ async def next_cmd(mc, cmds, json_output=False):
1839
2357
  if classic :
1840
2358
  print("→",end="")
1841
2359
  else :
1842
- print(f"{ANSI_NORMAL}🭬",end="")
2360
+ print(f"{ANSI_NORMAL}{ARROW_HEAD}",end="")
1843
2361
  print(ANSI_END, end="")
1844
2362
  if "hash" in t:
1845
2363
  print(f"[{t['hash']}]",end="")
@@ -1856,7 +2374,12 @@ async def next_cmd(mc, cmds, json_output=False):
1856
2374
  else:
1857
2375
  print(f"Unknown contact {cmds[1]}")
1858
2376
  else:
1859
- res = await mc.commands.send_login(contact, cmds[2])
2377
+ password = cmds[2]
2378
+ if password == "$":
2379
+ sess = PromptSession("Password: ", is_password=True)
2380
+ password = await sess.prompt_async()
2381
+
2382
+ res = await mc.commands.send_login(contact, password)
1860
2383
  logger.debug(res)
1861
2384
  if res.type == EventType.ERROR:
1862
2385
  if json_output :
@@ -1938,7 +2461,7 @@ async def next_cmd(mc, cmds, json_output=False):
1938
2461
  print("Timeout waiting telemetry")
1939
2462
  else :
1940
2463
  print(json.dumps(res.payload, indent=4))
1941
-
2464
+
1942
2465
  case "disc_path" | "dp" :
1943
2466
  argnum = 1
1944
2467
  await mc.ensure_contacts()
@@ -1959,6 +2482,71 @@ async def next_cmd(mc, cmds, json_output=False):
1959
2482
  inp = inp if inp != "" else "direct"
1960
2483
  print(f"Path for {contact['adv_name']}: out {outp}, in {inp}")
1961
2484
 
2485
+ case "node_discover"|"nd" :
2486
+ argnum = 1
2487
+ prefix_only = True
2488
+
2489
+ if len(cmds) == 1:
2490
+ argnum = 0
2491
+ types = 0xFF
2492
+ else:
2493
+ try: # try to decode type as int
2494
+ types = int(cmds[1])
2495
+ except ValueError:
2496
+ if "all" in cmds[1]:
2497
+ types = 0xFF
2498
+ else :
2499
+ types = 0
2500
+ if "rep" in cmds[1] or "rpt" in cmds[1]:
2501
+ types = types | 4
2502
+ if "cli" in cmds[1] or "comp" in cmds[1]:
2503
+ types = types | 2
2504
+ if "room" in cmds[1]:
2505
+ types = types | 8
2506
+ if "sens" in cmds[1]:
2507
+ types = types | 16
2508
+
2509
+ if "full" in cmds[1]:
2510
+ prefix_only = False
2511
+
2512
+ res = await mc.commands.send_node_discover_req(types, prefix_only=prefix_only)
2513
+ if res is None or res.type == EventType.ERROR:
2514
+ print("Error sending discover request")
2515
+ else:
2516
+ exp_tag = res.payload["tag"].to_bytes(4, "little").hex()
2517
+ dn = []
2518
+ while True:
2519
+ r = await mc.wait_for_event(
2520
+ EventType.DISCOVER_RESPONSE,
2521
+ attribute_filters={"tag":exp_tag},
2522
+ timeout = 5
2523
+ )
2524
+ if r is None or r.type == EventType.ERROR:
2525
+ break
2526
+ else:
2527
+ dn.append(r.payload)
2528
+
2529
+ if json_output:
2530
+ print(json.dumps(dn))
2531
+ else:
2532
+ await mc.ensure_contacts()
2533
+ print(f"Discovered {len(dn)} nodes:")
2534
+ for n in dn:
2535
+ name = f"{n['pubkey'][0:2]} {mc.get_contact_by_key_prefix(n['pubkey'])['adv_name']}"
2536
+ if name is None:
2537
+ name = n["pubkey"][0:16]
2538
+ type = f"t:{n['node_type']}"
2539
+ if n['node_type'] == 1:
2540
+ type = "CLI"
2541
+ elif n['node_type'] == 2:
2542
+ type = "REP"
2543
+ elif n['node_type'] == 3:
2544
+ type = "ROOM"
2545
+ elif n['node_type'] == 4:
2546
+ type = "SENS"
2547
+
2548
+ print(f" {name:16} {type:>4} SNR: {n['SNR_in']:6,.2f}->{n['SNR']:6,.2f} RSSI: ->{n['RSSI']:4}")
2549
+
1962
2550
  case "req_btelemetry"|"rbt" :
1963
2551
  argnum = 1
1964
2552
  await mc.ensure_contacts()
@@ -2089,6 +2677,13 @@ async def next_cmd(mc, cmds, json_output=False):
2089
2677
  case "add_pending":
2090
2678
  argnum = 1
2091
2679
  contact = mc.pop_pending_contact(cmds[1])
2680
+ if contact is None: # try to find by name
2681
+ key = None
2682
+ for c in mc.pending_contacts.items():
2683
+ if c[1]['adv_name'] == cmds[1]:
2684
+ key = c[1]['public_key']
2685
+ contact = mc.pop_pending_contact(key)
2686
+ break
2092
2687
  if contact is None:
2093
2688
  if json_output:
2094
2689
  print(json.dumps({"error":"Contact does not exist"}))
@@ -2368,7 +2963,7 @@ async def next_cmd(mc, cmds, json_output=False):
2368
2963
  if json_output:
2369
2964
  await ps.prompt_async()
2370
2965
  else:
2371
- await ps.prompt_async("Press Enter to continue ...")
2966
+ await ps.prompt_async("Press Enter to continue ...\n")
2372
2967
  except (EOFError, KeyboardInterrupt, asyncio.CancelledError):
2373
2968
  pass
2374
2969
 
@@ -2427,7 +3022,7 @@ async def next_cmd(mc, cmds, json_output=False):
2427
3022
  await mc.ensure_contacts()
2428
3023
  contact = mc.get_contact_by_name(cmds[0])
2429
3024
  if contact is None:
2430
- logger.error(f"Unknown command : {cmd}, will exit ...")
3025
+ logger.error(f"Unknown command : {cmd}, {cmds} not executed ...")
2431
3026
  return None
2432
3027
 
2433
3028
  await interactive_loop(mc, to=contact)
@@ -2436,7 +3031,10 @@ async def next_cmd(mc, cmds, json_output=False):
2436
3031
  return cmds[argnum+1:]
2437
3032
 
2438
3033
  except IndexError:
2439
- logger.error("Error in parameters, returning")
3034
+ logger.error("Error in parameters")
3035
+ return None
3036
+ except EOFError:
3037
+ logger.error("Cancelled")
2440
3038
  return None
2441
3039
 
2442
3040
  async def process_cmds (mc, args, json_output=False) :
@@ -2458,14 +3056,18 @@ async def process_script(mc, file, json_output=False):
2458
3056
  line = line.strip()
2459
3057
  if not (line == "" or line[0] == "#"):
2460
3058
  logger.debug(f"processing {line}")
2461
- cmds = shlex.split(line)
2462
- await process_cmds(mc, cmds, json_output)
3059
+ try :
3060
+ cmds = shlex.split(line)
3061
+ await process_cmds(mc, cmds, json_output)
3062
+ except ValueError:
3063
+ logger.error(f"Error processing {line}")
2463
3064
 
2464
3065
  def version():
2465
3066
  print (f"meshcore-cli: command line interface to MeshCore companion radios {VERSION}")
2466
3067
 
2467
3068
  def command_help():
2468
- print(""" General commands
3069
+ print(""" ?<cmd> may give you some more help about cmd
3070
+ General commands
2469
3071
  chat : enter the chat (interactive) mode
2470
3072
  chat_to <ct> : enter chat with contact to
2471
3073
  script <filename> : execute commands in filename
@@ -2476,6 +3078,7 @@ def command_help():
2476
3078
  reboot : reboots node
2477
3079
  sleep <secs> : sleeps for a given amount of secs s
2478
3080
  wait_key : wait until user presses <Enter> wk
3081
+ apply_to <scope> <cmds>: sends cmds to contacts matching scope at
2479
3082
  Messenging
2480
3083
  msg <name> <msg> : send message to node by name m {
2481
3084
  wait_ack : wait an ack wa }
@@ -2497,6 +3100,7 @@ def command_help():
2497
3100
  time <epoch> : sets time to given epoch
2498
3101
  clock : get current time
2499
3102
  clock sync : sync device clock st
3103
+ node_discover <filter> : discovers nodes based on their type nd
2500
3104
  Contacts
2501
3105
  contacts / list : gets contact list lc
2502
3106
  reload_contacts : force reloading all contacts rc
@@ -2515,7 +3119,7 @@ def command_help():
2515
3119
  req_mma <ct> : requests min/max/avg for a sensor rm
2516
3120
  req_acl <ct> : requests access control list for sensor
2517
3121
  pending_contacts : show pending contacts
2518
- add_pending <key> : manually add pending contact from key
3122
+ add_pending <pending> : manually add pending contact
2519
3123
  flush_pending : flush pending contact list
2520
3124
  Repeaters
2521
3125
  login <name> <pwd> : log into a node (rep) with given pwd l
@@ -2550,6 +3154,43 @@ def usage () :
2550
3154
  Available Commands and shorcuts (can be chained) :""")
2551
3155
  command_help()
2552
3156
 
3157
+ def get_help_for (cmdname, context="line") :
3158
+ if cmdname == "apply_to" or cmdname == "at" :
3159
+ print("""apply_to <scope> <cmd> : applies cmd to contacts matching scope
3160
+ Scope acts like a filter with comma separated fields :
3161
+ - u, matches modification time < or > than a timestamp
3162
+ (can also be days hours or minutes ago if followed by d,h or m)
3163
+ - t, matches the type (1: client, 2: repeater, 3: room, 4: sensor)
3164
+ - h, matches number of hops
3165
+ - d, direct, similar to h>-1
3166
+ - f, flood, similar to h<0 or h=-1
3167
+
3168
+ Note: Some commands like contact_name (aka cn), reset_path (aka rp), forget_password (aka fp) can be chained.
3169
+
3170
+ Examples:
3171
+ # removes all clients that have not been updated in last 2 days
3172
+ at u<2d,t=1 remove_contact
3173
+ # gives traces to repeaters that have been updated in the last 24h and are direct
3174
+ at t=2,u>1d,d cn trace
3175
+ # tries to do flood login to all repeaters
3176
+ at t=2 rp login
3177
+ """)
3178
+
3179
+ if cmdname == "node_discover" or cmdname == "nd" :
3180
+ print("""node_discover <filter> : discovers 0-hop nodes and displays signal info
3181
+
3182
+ filter can be "all" for all types or nodes or a comma separated list consisting of :
3183
+ - cli or comp for companions
3184
+ - rep for repeaters
3185
+ - sens for sensors
3186
+ - room for chat rooms
3187
+
3188
+ nd can be used with no filter parameter ... !!! BEWARE WITH CHAINING !!!
3189
+ """)
3190
+
3191
+ else:
3192
+ print(f"Sorry, no help yet for {cmdname}")
3193
+
2553
3194
  async def main(argv):
2554
3195
  """ Do the job """
2555
3196
  json_output = JSON
@@ -2568,7 +3209,7 @@ async def main(argv):
2568
3209
  with open(MCCLI_ADDRESS, encoding="utf-8") as f :
2569
3210
  address = f.readline().strip()
2570
3211
 
2571
- opts, args = getopt.getopt(argv, "a:d:s:ht:p:b:jDhvSlT:P")
3212
+ opts, args = getopt.getopt(argv, "a:d:s:ht:p:b:fjDhvSlT:P")
2572
3213
  for opt, arg in opts :
2573
3214
  match opt:
2574
3215
  case "-d" : # name specified on cmdline
@@ -2598,25 +3239,34 @@ async def main(argv):
2598
3239
  case "-v":
2599
3240
  version()
2600
3241
  return
3242
+ case "-f": # connect to first encountered device
3243
+ address = ""
2601
3244
  case "-l" :
2602
3245
  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}")
3246
+ try :
3247
+ devices = await BleakScanner.discover(timeout=timeout)
3248
+ if len(devices) == 0:
3249
+ print(" No ble device found")
3250
+ for d in devices :
3251
+ if not d.name is None and d.name.startswith("MeshCore-"):
3252
+ print(f" {d.address} {d.name}")
3253
+ except BleakError:
3254
+ print(" No BLE HW")
2609
3255
  print("\nSerial ports:")
2610
3256
  ports = serial.tools.list_ports.comports()
2611
3257
  for port, desc, hwid in sorted(ports):
2612
3258
  print(f" {port:<18} {desc} [{hwid}]")
2613
3259
  return
2614
3260
  case "-S" :
2615
- devices = await BleakScanner.discover(timeout=timeout)
2616
3261
  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}"))
3262
+
3263
+ try :
3264
+ devices = await BleakScanner.discover(timeout=timeout)
3265
+ for d in devices:
3266
+ if not d.name is None and d.name.startswith("MeshCore-"):
3267
+ choices.append(({"type":"ble","device":d}, f"{d.address:<22} {d.name}"))
3268
+ except BleakError:
3269
+ logger.info("No BLE Device")
2620
3270
 
2621
3271
  ports = serial.tools.list_ports.comports()
2622
3272
  for port, desc, hwid in sorted(ports):
@@ -2642,7 +3292,7 @@ async def main(argv):
2642
3292
  else:
2643
3293
  logger.error("Invalid choice")
2644
3294
  return
2645
-
3295
+
2646
3296
  if (debug==True):
2647
3297
  logger.setLevel(logging.DEBUG)
2648
3298
  elif (json_output) :
@@ -2660,7 +3310,10 @@ async def main(argv):
2660
3310
  elif address and len(address) == 36 and len(address.split("-")) == 5:
2661
3311
  client = BleakClient(address) # mac uses uuid, we'll pass a client
2662
3312
  else:
2663
- logger.info(f"Scanning BLE for device matching {address}")
3313
+ if address == "":
3314
+ logger.info(f"Searching first MC BLE device")
3315
+ else:
3316
+ logger.info(f"Scanning BLE for device matching {address}")
2664
3317
  devices = await BleakScanner.discover(timeout=timeout)
2665
3318
  found = False
2666
3319
  for d in devices:
@@ -2685,6 +3338,26 @@ async def main(argv):
2685
3338
  mc = await MeshCore.create_ble(address=address, device=device, client=client, debug=debug, only_error=json_output, pin=pin)
2686
3339
  except ConnectionError :
2687
3340
  logger.info("Error while connecting, retrying once ...")
3341
+ if device is None and client is None: # Search for device
3342
+ logger.info(f"Scanning BLE for device matching {address}")
3343
+ devices = await BleakScanner.discover(timeout=timeout)
3344
+ found = False
3345
+ for d in devices:
3346
+ if not d.name is None and d.name.startswith("MeshCore-") and\
3347
+ (address is None or address in d.name) :
3348
+ address=d.address
3349
+ device=d
3350
+ logger.info(f"Found device {d.name} {d.address}")
3351
+ found = True
3352
+ break
3353
+ elif d.address == address : # on a mac, address is an uuid
3354
+ device = d
3355
+ logger.info(f"Found device {d.name} {d.address}")
3356
+ found = True
3357
+ break
3358
+ if not found :
3359
+ logger.info(f"Couldn't find device {address}")
3360
+ return
2688
3361
  try :
2689
3362
  mc = await MeshCore.create_ble(address=address, device=device, client=client, debug=debug, only_error=json_output, pin=pin)
2690
3363
  except ConnectionError :
@@ -2703,10 +3376,12 @@ async def main(argv):
2703
3376
  handle_message.mc = mc # connect meshcore to handle_message
2704
3377
  handle_advert.mc = mc
2705
3378
  handle_path_update.mc = mc
3379
+ handle_log_rx.mc = mc
2706
3380
 
2707
3381
  mc.subscribe(EventType.ADVERTISEMENT, handle_advert)
2708
3382
  mc.subscribe(EventType.PATH_UPDATE, handle_path_update)
2709
3383
  mc.subscribe(EventType.NEW_CONTACT, handle_new_contact)
3384
+ mc.subscribe(EventType.RX_LOG_DATA, handle_log_rx)
2710
3385
 
2711
3386
  mc.auto_update_contacts = True
2712
3387