meshcore-cli 1.2.1__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.
- meshcore_cli/meshcore_cli.py +539 -42
- {meshcore_cli-1.2.1.dist-info → meshcore_cli-1.2.10.dist-info}/METADATA +3 -2
- meshcore_cli-1.2.10.dist-info/RECORD +8 -0
- meshcore_cli-1.2.1.dist-info/RECORD +0 -8
- {meshcore_cli-1.2.1.dist-info → meshcore_cli-1.2.10.dist-info}/WHEEL +0 -0
- {meshcore_cli-1.2.1.dist-info → meshcore_cli-1.2.10.dist-info}/entry_points.txt +0 -0
- {meshcore_cli-1.2.1.dist-info → meshcore_cli-1.2.10.dist-info}/licenses/LICENSE +0 -0
meshcore_cli/meshcore_cli.py
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import asyncio
|
|
7
|
-
import os, sys
|
|
7
|
+
import os, sys, io, platform
|
|
8
8
|
import time, datetime
|
|
9
9
|
import getopt, json, shlex, re
|
|
10
10
|
import logging
|
|
@@ -24,13 +24,16 @@ from prompt_toolkit.key_binding import KeyBindings
|
|
|
24
24
|
from prompt_toolkit.shortcuts import radiolist_dialog
|
|
25
25
|
from prompt_toolkit.completion.word_completer import WordCompleter
|
|
26
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
|
|
27
30
|
|
|
28
31
|
import re
|
|
29
32
|
|
|
30
33
|
from meshcore import MeshCore, EventType, logger
|
|
31
34
|
|
|
32
35
|
# Version
|
|
33
|
-
VERSION = "v1.2.
|
|
36
|
+
VERSION = "v1.2.10"
|
|
34
37
|
|
|
35
38
|
# default ble address is stored in a config file
|
|
36
39
|
MCCLI_CONFIG_DIR = str(Path.home()) + "/.config/meshcore/"
|
|
@@ -52,6 +55,7 @@ ANSI_INVERT = "\033[7m"
|
|
|
52
55
|
ANSI_NORMAL = "\033[27m"
|
|
53
56
|
ANSI_GREEN = "\033[0;32m"
|
|
54
57
|
ANSI_BGREEN = "\033[1;32m"
|
|
58
|
+
ANSI_DGREEN="\033[0;38;5;22m"
|
|
55
59
|
ANSI_BLUE = "\033[0;34m"
|
|
56
60
|
ANSI_BBLUE = "\033[1;34m"
|
|
57
61
|
ANSI_RED = "\033[0;31m"
|
|
@@ -72,6 +76,15 @@ ANSI_BORANGE="\033[1;38;5;214m"
|
|
|
72
76
|
ANSI_YELLOW = "\033[0;33m"
|
|
73
77
|
ANSI_BYELLOW = "\033[1;33m"
|
|
74
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
|
+
|
|
75
88
|
def escape_ansi(line):
|
|
76
89
|
ansi_escape = re.compile(r'(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]')
|
|
77
90
|
return ansi_escape.sub('', line)
|
|
@@ -193,6 +206,66 @@ process_event_message.print_snr=False
|
|
|
193
206
|
process_event_message.color=True
|
|
194
207
|
process_event_message.last_node=None
|
|
195
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
|
+
|
|
196
269
|
async def handle_advert(event):
|
|
197
270
|
if not handle_advert.print_adverts:
|
|
198
271
|
return
|
|
@@ -264,7 +337,7 @@ async def log_message(mc, msg):
|
|
|
264
337
|
if msg["type"] == "PRIV" :
|
|
265
338
|
ct = mc.get_contact_by_key_prefix(msg['pubkey_prefix'])
|
|
266
339
|
if ct is None:
|
|
267
|
-
msg["name"] =
|
|
340
|
+
msg["name"] = msg["pubkey_prefix"]
|
|
268
341
|
else:
|
|
269
342
|
msg["name"] = ct["adv_name"]
|
|
270
343
|
elif msg["type"] == "CHAN" :
|
|
@@ -308,7 +381,7 @@ async def subscribe_to_msgs(mc, json_output=False, above=False):
|
|
|
308
381
|
class MyNestedCompleter(NestedCompleter):
|
|
309
382
|
def get_completions( self, document, complete_event):
|
|
310
383
|
txt = document.text_before_cursor.lstrip()
|
|
311
|
-
if not " " in txt:
|
|
384
|
+
if not " " in txt:
|
|
312
385
|
if txt != "" and txt[0] == "/" and txt.count("/") == 1:
|
|
313
386
|
opts = []
|
|
314
387
|
for k in self.options.keys():
|
|
@@ -320,7 +393,7 @@ class MyNestedCompleter(NestedCompleter):
|
|
|
320
393
|
opts = self.options.keys()
|
|
321
394
|
completer = WordCompleter(
|
|
322
395
|
opts, ignore_case=self.ignore_case,
|
|
323
|
-
pattern=re.compile(r"([a-zA-Z0-9_
|
|
396
|
+
pattern=re.compile(r"([a-zA-Z0-9_\\/\#]+|[^a-zA-Z0-9_\s\#]+)"))
|
|
324
397
|
yield from completer.get_completions(document, complete_event)
|
|
325
398
|
else: # normal behavior for remainder
|
|
326
399
|
yield from super().get_completions(document, complete_event)
|
|
@@ -341,6 +414,10 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
|
|
|
341
414
|
for c in it :
|
|
342
415
|
contact_list[c[1]['adv_name']] = None
|
|
343
416
|
|
|
417
|
+
pit = iter(pending.items())
|
|
418
|
+
for c in pit :
|
|
419
|
+
pending_list[c[1]['adv_name']] = None
|
|
420
|
+
|
|
344
421
|
pit = iter(pending.items())
|
|
345
422
|
for c in pit :
|
|
346
423
|
pending_list[c[1]['public_key']] = None
|
|
@@ -383,6 +460,7 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
|
|
|
383
460
|
"share_contact" : contact_list,
|
|
384
461
|
"path": contact_list,
|
|
385
462
|
"disc_path" : contact_list,
|
|
463
|
+
"node_discover": {"all":None, "sens":None, "rep":None, "comp":None, "room":None, "cli":None},
|
|
386
464
|
"trace" : None,
|
|
387
465
|
"reset_path" : contact_list,
|
|
388
466
|
"change_path" : contact_list,
|
|
@@ -403,6 +481,9 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
|
|
|
403
481
|
"set_channel": None,
|
|
404
482
|
"get_channels": None,
|
|
405
483
|
"remove_channel": None,
|
|
484
|
+
"apply_to": None,
|
|
485
|
+
"at": None,
|
|
486
|
+
"scope": None,
|
|
406
487
|
"set" : {
|
|
407
488
|
"name" : None,
|
|
408
489
|
"pin" : None,
|
|
@@ -417,6 +498,9 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
|
|
|
417
498
|
"color" : {"on":None, "off":None},
|
|
418
499
|
"print_name" : {"on":None, "off":None},
|
|
419
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},
|
|
420
504
|
"print_new_contacts" : {"on": None, "off":None},
|
|
421
505
|
"print_path_updates" : {"on":None,"off":None},
|
|
422
506
|
"classic_prompt" : {"on" : None, "off":None},
|
|
@@ -444,6 +528,9 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
|
|
|
444
528
|
"color":None,
|
|
445
529
|
"print_name":None,
|
|
446
530
|
"print_adverts":None,
|
|
531
|
+
"json_log_rx":None,
|
|
532
|
+
"channel_echoes":None,
|
|
533
|
+
"echo_unk_chans":None,
|
|
447
534
|
"print_path_updates":None,
|
|
448
535
|
"print_new_contacts":None,
|
|
449
536
|
"classic_prompt":None,
|
|
@@ -463,6 +550,8 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
|
|
|
463
550
|
|
|
464
551
|
contact_completion_list = {
|
|
465
552
|
"contact_info": None,
|
|
553
|
+
"contact_name": None,
|
|
554
|
+
"contact_lastmod": None,
|
|
466
555
|
"export_contact" : None,
|
|
467
556
|
"share_contact" : None,
|
|
468
557
|
"upload_contact" : None,
|
|
@@ -501,6 +590,7 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
|
|
|
501
590
|
"neighbors" : None,
|
|
502
591
|
"req_acl":None,
|
|
503
592
|
"setperm":contact_list,
|
|
593
|
+
"region" : {"get":None, "allowf": None, "denyf": None, "put": None, "remove": None, "save": None, "home": None},
|
|
504
594
|
"gps" : {"on":None,"off":None,"sync":None,"setloc":None,
|
|
505
595
|
"advert" : {"none": None, "share": None, "prefs": None},
|
|
506
596
|
},
|
|
@@ -584,7 +674,7 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
|
|
|
584
674
|
slash_root_completion_list = {}
|
|
585
675
|
for k,v in root_completion_list.items():
|
|
586
676
|
slash_root_completion_list["/"+k]=v
|
|
587
|
-
|
|
677
|
+
|
|
588
678
|
completion_list.update(slash_root_completion_list)
|
|
589
679
|
|
|
590
680
|
slash_contacts_completion_list = {}
|
|
@@ -604,6 +694,14 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
|
|
|
604
694
|
|
|
605
695
|
completion_list.update(slash_contacts_completion_list)
|
|
606
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)
|
|
704
|
+
|
|
607
705
|
completion_list.update({
|
|
608
706
|
"script" : None,
|
|
609
707
|
"quit" : None
|
|
@@ -621,6 +719,8 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
621
719
|
contact = to
|
|
622
720
|
prev_contact = None
|
|
623
721
|
|
|
722
|
+
scope = await set_scope(mc, "*")
|
|
723
|
+
|
|
624
724
|
await get_contacts(mc, anim=True)
|
|
625
725
|
await get_channels(mc, anim=True)
|
|
626
726
|
await subscribe_to_msgs(mc, above=True)
|
|
@@ -659,6 +759,9 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
659
759
|
|
|
660
760
|
last_ack = True
|
|
661
761
|
while True:
|
|
762
|
+
# reset scope (if changed)
|
|
763
|
+
scope = await set_scope(mc, scope)
|
|
764
|
+
|
|
662
765
|
color = process_event_message.color
|
|
663
766
|
classic = interactive_loop.classic or not color
|
|
664
767
|
print_name = interactive_loop.print_name
|
|
@@ -668,14 +771,16 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
668
771
|
else:
|
|
669
772
|
prompt = f"{ANSI_INVERT}"
|
|
670
773
|
|
|
671
|
-
# some possible symbols for prompts 🭬🬛🬗🭬🬛🬃🬗🭬🬛🬃🬗🬏🭀🭋🭨🮋
|
|
672
774
|
if print_name or contact is None :
|
|
673
775
|
prompt = prompt + f"{ANSI_BGRAY}"
|
|
674
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}"
|
|
675
780
|
if classic :
|
|
676
781
|
prompt = prompt + " > "
|
|
677
782
|
else :
|
|
678
|
-
prompt = prompt + "
|
|
783
|
+
prompt = prompt + f"{ANSI_NORMAL}{ARROW_HEAD}{ANSI_INVERT}"
|
|
679
784
|
|
|
680
785
|
if not contact is None :
|
|
681
786
|
if not last_ack:
|
|
@@ -696,13 +801,24 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
696
801
|
prompt = prompt + f"{ANSI_INVERT}"
|
|
697
802
|
|
|
698
803
|
if print_name and not classic :
|
|
699
|
-
prompt = prompt + "
|
|
804
|
+
prompt = prompt + f"{ANSI_NORMAL}{ARROW_TAIL}{ANSI_INVERT}"
|
|
700
805
|
|
|
701
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
|
+
|
|
702
818
|
if classic :
|
|
703
819
|
prompt = prompt + f"{ANSI_NORMAL} > "
|
|
704
820
|
else:
|
|
705
|
-
prompt = prompt + f"{ANSI_NORMAL}
|
|
821
|
+
prompt = prompt + f"{ANSI_NORMAL}{ARROW_HEAD}"
|
|
706
822
|
|
|
707
823
|
prompt = prompt + f"{ANSI_END}"
|
|
708
824
|
|
|
@@ -723,36 +839,91 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
723
839
|
completer=completer,
|
|
724
840
|
key_bindings=bindings)
|
|
725
841
|
|
|
842
|
+
line = line.strip()
|
|
843
|
+
|
|
726
844
|
if line == "" : # blank line
|
|
727
845
|
pass
|
|
728
846
|
|
|
847
|
+
elif line.startswith("?") :
|
|
848
|
+
get_help_for(line[1:], context="chat")
|
|
849
|
+
|
|
729
850
|
# raw meshcli command as on command line
|
|
730
851
|
elif line.startswith("$") :
|
|
731
|
-
|
|
732
|
-
|
|
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")
|
|
733
874
|
|
|
734
875
|
elif line.startswith("/") :
|
|
735
876
|
path = line.split(" ", 1)[0]
|
|
736
877
|
if path.count("/") == 1:
|
|
737
|
-
args =
|
|
738
|
-
|
|
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:]}")
|
|
739
900
|
else:
|
|
740
901
|
cmdline = line[1:].split("/",1)[1]
|
|
741
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)
|
|
742
908
|
tct = mc.get_contact_by_name(contact_name)
|
|
743
909
|
if tct is None:
|
|
744
910
|
print(f"{contact_name} is not a contact")
|
|
745
911
|
else:
|
|
746
912
|
if not await process_contact_chat_line(mc, tct, cmdline):
|
|
747
|
-
if
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
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])
|
|
751
918
|
|
|
752
919
|
elif line.startswith("to ") : # dest
|
|
753
920
|
dest = line[3:]
|
|
754
921
|
if dest.startswith("\"") or dest.startswith("\'") : # if name starts with a quote
|
|
755
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]
|
|
756
927
|
nc = mc.get_contact_by_name(dest)
|
|
757
928
|
if nc is None:
|
|
758
929
|
if dest == "public" :
|
|
@@ -766,6 +937,8 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
766
937
|
nc["adv_name"] = mc.channels[dest]["channel_name"]
|
|
767
938
|
elif dest == ".." : # previous recipient
|
|
768
939
|
nc = prev_contact
|
|
940
|
+
if dest_scope is None and not scope is None:
|
|
941
|
+
dest_scope = prev_scope
|
|
769
942
|
elif dest == "~" or dest == "/" or dest == mc.self_info['name']:
|
|
770
943
|
nc = None
|
|
771
944
|
elif dest == "!" :
|
|
@@ -783,6 +956,12 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
783
956
|
last_ack = True
|
|
784
957
|
prev_contact = contact
|
|
785
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)
|
|
786
965
|
|
|
787
966
|
elif line == "to" :
|
|
788
967
|
if contact is None :
|
|
@@ -805,7 +984,7 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
805
984
|
if ln is None :
|
|
806
985
|
print("No received msg yet !")
|
|
807
986
|
elif ln["type"] == 0 :
|
|
808
|
-
await
|
|
987
|
+
await send_chan_msg(mc, ln["chan_nb"], line[1:])
|
|
809
988
|
else :
|
|
810
989
|
last_ack = await msg_ack(mc, ln, line[1:])
|
|
811
990
|
if last_ack == False :
|
|
@@ -813,8 +992,11 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
813
992
|
|
|
814
993
|
# commands are passed through if at root
|
|
815
994
|
elif contact is None or line.startswith(".") :
|
|
816
|
-
|
|
817
|
-
|
|
995
|
+
try:
|
|
996
|
+
args = shlex.split(line)
|
|
997
|
+
await process_cmds(mc, args)
|
|
998
|
+
except ValueError:
|
|
999
|
+
logger.error(f"Error processing {line}")
|
|
818
1000
|
|
|
819
1001
|
elif await process_contact_chat_line(mc, contact, line):
|
|
820
1002
|
pass
|
|
@@ -837,7 +1019,7 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
837
1019
|
last_ack = await msg_ack(mc, contact, line)
|
|
838
1020
|
|
|
839
1021
|
elif contact["type"] == 0 : # channel, send msg to channel
|
|
840
|
-
await
|
|
1022
|
+
await send_chan_msg(mc, contact["chan_nb"], line)
|
|
841
1023
|
|
|
842
1024
|
elif contact["type"] == 1 : # chat, send to recipient and wait ack
|
|
843
1025
|
last_ack = await msg_ack(mc, contact, line)
|
|
@@ -859,6 +1041,12 @@ async def process_contact_chat_line(mc, contact, line):
|
|
|
859
1041
|
if contact["type"] == 0:
|
|
860
1042
|
return False
|
|
861
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
|
+
|
|
862
1050
|
if line.startswith(":") : # : will send a command to current recipient
|
|
863
1051
|
args=["cmd", contact['adv_name'], line[1:]]
|
|
864
1052
|
await process_cmds(mc, args)
|
|
@@ -869,6 +1057,23 @@ async def process_contact_chat_line(mc, contact, line):
|
|
|
869
1057
|
await process_cmds(mc, args)
|
|
870
1058
|
return True
|
|
871
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
|
+
|
|
872
1077
|
# commands that take contact as second arg will be sent to recipient
|
|
873
1078
|
if line == "sc" or line == "share_contact" or\
|
|
874
1079
|
line == "ec" or line == "export_contact" or\
|
|
@@ -956,28 +1161,45 @@ async def process_contact_chat_line(mc, contact, line):
|
|
|
956
1161
|
return True
|
|
957
1162
|
|
|
958
1163
|
# same but for commands with a parameter
|
|
959
|
-
if
|
|
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 ") :
|
|
1164
|
+
if " " in line:
|
|
964
1165
|
cmds = line.split(" ", 1)
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
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
|
|
968
1179
|
|
|
969
1180
|
if line == "login": # use stored password or prompt for it
|
|
970
1181
|
password_file = ""
|
|
971
1182
|
password = ""
|
|
972
1183
|
if os.path.isdir(MCCLI_CONFIG_DIR) :
|
|
1184
|
+
# if a password file exists with node name open it and destroy it
|
|
973
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"
|
|
974
1196
|
if os.path.exists(password_file) :
|
|
975
1197
|
with open(password_file, "r", encoding="utf-8") as f :
|
|
976
1198
|
password=f.readline().strip()
|
|
977
1199
|
|
|
978
1200
|
if password == "":
|
|
979
1201
|
try:
|
|
980
|
-
sess = PromptSession("Password: ", is_password=True)
|
|
1202
|
+
sess = PromptSession(f"Password for {contact['adv_name']}: ", is_password=True)
|
|
981
1203
|
password = await sess.prompt_async()
|
|
982
1204
|
except EOFError:
|
|
983
1205
|
logger.info("Canceled")
|
|
@@ -993,6 +1215,9 @@ async def process_contact_chat_line(mc, contact, line):
|
|
|
993
1215
|
|
|
994
1216
|
if line.startswith("forget_password") or line.startswith("fp"):
|
|
995
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"
|
|
996
1221
|
if os.path.exists(password_file):
|
|
997
1222
|
os.remove(password_file)
|
|
998
1223
|
try:
|
|
@@ -1013,6 +1238,89 @@ async def process_contact_chat_line(mc, contact, line):
|
|
|
1013
1238
|
|
|
1014
1239
|
return False
|
|
1015
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']}")
|
|
1016
1324
|
|
|
1017
1325
|
async def send_cmd (mc, contact, cmd) :
|
|
1018
1326
|
res = await mc.commands.send_cmd(contact, cmd)
|
|
@@ -1056,7 +1364,7 @@ async def send_msg (mc, contact, msg) :
|
|
|
1056
1364
|
|
|
1057
1365
|
async def msg_ack (mc, contact, msg) :
|
|
1058
1366
|
timeout = 0 if not 'timeout' in contact else contact['timeout']
|
|
1059
|
-
res = await mc.commands.send_msg_with_retry(contact, msg,
|
|
1367
|
+
res = await mc.commands.send_msg_with_retry(contact, msg,
|
|
1060
1368
|
max_attempts=msg_ack.max_attempts,
|
|
1061
1369
|
flood_after=msg_ack.flood_after,
|
|
1062
1370
|
max_flood_attempts=msg_ack.max_flood_attempts,
|
|
@@ -1076,6 +1384,28 @@ msg_ack.max_attempts=3
|
|
|
1076
1384
|
msg_ack.flood_after=2
|
|
1077
1385
|
msg_ack.max_flood_attempts=1
|
|
1078
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
|
+
|
|
1079
1409
|
async def get_channel (mc, chan) :
|
|
1080
1410
|
if not chan.isnumeric():
|
|
1081
1411
|
return await get_channel_by_name(mc, chan)
|
|
@@ -1112,6 +1442,7 @@ async def set_channel (mc, chan, name, key=None):
|
|
|
1112
1442
|
return None
|
|
1113
1443
|
|
|
1114
1444
|
info = res.payload
|
|
1445
|
+
info["channel_hash"] = sha256(info["channel_secret"]).digest()[0:1].hex()
|
|
1115
1446
|
info["channel_secret"] = info["channel_secret"].hex()
|
|
1116
1447
|
|
|
1117
1448
|
if hasattr(mc,'channels') :
|
|
@@ -1200,12 +1531,14 @@ async def get_channels (mc, anim=False) :
|
|
|
1200
1531
|
if res.type == EventType.ERROR:
|
|
1201
1532
|
break
|
|
1202
1533
|
info = res.payload
|
|
1534
|
+
info["channel_hash"] = sha256(info["channel_secret"]).digest()[0:1].hex()
|
|
1203
1535
|
info["channel_secret"] = info["channel_secret"].hex()
|
|
1204
1536
|
mc.channels.append(info)
|
|
1205
1537
|
ch = ch + 1
|
|
1206
1538
|
if anim:
|
|
1207
1539
|
print(".", end="", flush=True)
|
|
1208
|
-
|
|
1540
|
+
if anim:
|
|
1541
|
+
print (" Done")
|
|
1209
1542
|
return mc.channels
|
|
1210
1543
|
|
|
1211
1544
|
async def print_trace_to (mc, contact):
|
|
@@ -1279,8 +1612,14 @@ async def print_disc_trace_to (mc, contact):
|
|
|
1279
1612
|
|
|
1280
1613
|
async def next_cmd(mc, cmds, json_output=False):
|
|
1281
1614
|
""" process next command """
|
|
1615
|
+
global ARROW_TAIL, ARROW_HEAD
|
|
1282
1616
|
try :
|
|
1283
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
|
+
|
|
1284
1623
|
if cmds[0].startswith(".") : # override json_output
|
|
1285
1624
|
json_output = True
|
|
1286
1625
|
cmd = cmds[0][1:]
|
|
@@ -1371,6 +1710,10 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
1371
1710
|
else:
|
|
1372
1711
|
print("Time set")
|
|
1373
1712
|
|
|
1713
|
+
case "apply_to"|"at":
|
|
1714
|
+
argnum = 2
|
|
1715
|
+
await apply_command_to_contacts(mc, cmds[1], cmds[2])
|
|
1716
|
+
|
|
1374
1717
|
case "set":
|
|
1375
1718
|
argnum = 2
|
|
1376
1719
|
match cmds[1]:
|
|
@@ -1403,6 +1746,10 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
1403
1746
|
interactive_loop.classic = (cmds[2] == "on")
|
|
1404
1747
|
if json_output :
|
|
1405
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]
|
|
1406
1753
|
case "color" :
|
|
1407
1754
|
process_event_message.color = (cmds[2] == "on")
|
|
1408
1755
|
if json_output :
|
|
@@ -1411,6 +1758,18 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
1411
1758
|
process_event_message.print_snr = (cmds[2] == "on")
|
|
1412
1759
|
if json_output :
|
|
1413
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]}))
|
|
1414
1773
|
case "print_adverts" :
|
|
1415
1774
|
handle_advert.print_adverts = (cmds[2] == "on")
|
|
1416
1775
|
if json_output :
|
|
@@ -1641,6 +2000,21 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
1641
2000
|
print(json.dumps({"color" : process_event_message.color}))
|
|
1642
2001
|
else:
|
|
1643
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'}")
|
|
1644
2018
|
case "print_adverts":
|
|
1645
2019
|
if json_output :
|
|
1646
2020
|
print(json.dumps({"print_adverts" : handle_advert.print_adverts}))
|
|
@@ -1838,6 +2212,12 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
1838
2212
|
if res is None:
|
|
1839
2213
|
print("Error setting channel")
|
|
1840
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
|
+
|
|
1841
2221
|
case "remove_channel":
|
|
1842
2222
|
argnum = 1
|
|
1843
2223
|
res = await set_channel(mc, cmds[1], "", bytes.fromhex(16*"00"))
|
|
@@ -1904,7 +2284,7 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
1904
2284
|
argnum = 2
|
|
1905
2285
|
dest = None
|
|
1906
2286
|
|
|
1907
|
-
if len(cmds[1]) == 12: # possibly an hex prefix
|
|
2287
|
+
if len(cmds[1]) == 12: # possibly an hex prefix
|
|
1908
2288
|
try:
|
|
1909
2289
|
dest = bytes.fromhex(cmds[1])
|
|
1910
2290
|
except ValueError:
|
|
@@ -1977,7 +2357,7 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
1977
2357
|
if classic :
|
|
1978
2358
|
print("→",end="")
|
|
1979
2359
|
else :
|
|
1980
|
-
print(f"{ANSI_NORMAL}
|
|
2360
|
+
print(f"{ANSI_NORMAL}{ARROW_HEAD}",end="")
|
|
1981
2361
|
print(ANSI_END, end="")
|
|
1982
2362
|
if "hash" in t:
|
|
1983
2363
|
print(f"[{t['hash']}]",end="")
|
|
@@ -2102,6 +2482,71 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
2102
2482
|
inp = inp if inp != "" else "direct"
|
|
2103
2483
|
print(f"Path for {contact['adv_name']}: out {outp}, in {inp}")
|
|
2104
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
|
+
|
|
2105
2550
|
case "req_btelemetry"|"rbt" :
|
|
2106
2551
|
argnum = 1
|
|
2107
2552
|
await mc.ensure_contacts()
|
|
@@ -2232,6 +2677,13 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
2232
2677
|
case "add_pending":
|
|
2233
2678
|
argnum = 1
|
|
2234
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
|
|
2235
2687
|
if contact is None:
|
|
2236
2688
|
if json_output:
|
|
2237
2689
|
print(json.dumps({"error":"Contact does not exist"}))
|
|
@@ -2511,7 +2963,7 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
2511
2963
|
if json_output:
|
|
2512
2964
|
await ps.prompt_async()
|
|
2513
2965
|
else:
|
|
2514
|
-
await ps.prompt_async("Press Enter to continue
|
|
2966
|
+
await ps.prompt_async("Press Enter to continue ...\n")
|
|
2515
2967
|
except (EOFError, KeyboardInterrupt, asyncio.CancelledError):
|
|
2516
2968
|
pass
|
|
2517
2969
|
|
|
@@ -2570,7 +3022,7 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
2570
3022
|
await mc.ensure_contacts()
|
|
2571
3023
|
contact = mc.get_contact_by_name(cmds[0])
|
|
2572
3024
|
if contact is None:
|
|
2573
|
-
logger.error(f"Unknown command : {cmd},
|
|
3025
|
+
logger.error(f"Unknown command : {cmd}, {cmds} not executed ...")
|
|
2574
3026
|
return None
|
|
2575
3027
|
|
|
2576
3028
|
await interactive_loop(mc, to=contact)
|
|
@@ -2579,7 +3031,7 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
2579
3031
|
return cmds[argnum+1:]
|
|
2580
3032
|
|
|
2581
3033
|
except IndexError:
|
|
2582
|
-
logger.error("Error in parameters
|
|
3034
|
+
logger.error("Error in parameters")
|
|
2583
3035
|
return None
|
|
2584
3036
|
except EOFError:
|
|
2585
3037
|
logger.error("Cancelled")
|
|
@@ -2604,14 +3056,18 @@ async def process_script(mc, file, json_output=False):
|
|
|
2604
3056
|
line = line.strip()
|
|
2605
3057
|
if not (line == "" or line[0] == "#"):
|
|
2606
3058
|
logger.debug(f"processing {line}")
|
|
2607
|
-
|
|
2608
|
-
|
|
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}")
|
|
2609
3064
|
|
|
2610
3065
|
def version():
|
|
2611
3066
|
print (f"meshcore-cli: command line interface to MeshCore companion radios {VERSION}")
|
|
2612
3067
|
|
|
2613
3068
|
def command_help():
|
|
2614
|
-
print("""
|
|
3069
|
+
print(""" ?<cmd> may give you some more help about cmd
|
|
3070
|
+
General commands
|
|
2615
3071
|
chat : enter the chat (interactive) mode
|
|
2616
3072
|
chat_to <ct> : enter chat with contact to
|
|
2617
3073
|
script <filename> : execute commands in filename
|
|
@@ -2622,6 +3078,7 @@ def command_help():
|
|
|
2622
3078
|
reboot : reboots node
|
|
2623
3079
|
sleep <secs> : sleeps for a given amount of secs s
|
|
2624
3080
|
wait_key : wait until user presses <Enter> wk
|
|
3081
|
+
apply_to <scope> <cmds>: sends cmds to contacts matching scope at
|
|
2625
3082
|
Messenging
|
|
2626
3083
|
msg <name> <msg> : send message to node by name m {
|
|
2627
3084
|
wait_ack : wait an ack wa }
|
|
@@ -2643,6 +3100,7 @@ def command_help():
|
|
|
2643
3100
|
time <epoch> : sets time to given epoch
|
|
2644
3101
|
clock : get current time
|
|
2645
3102
|
clock sync : sync device clock st
|
|
3103
|
+
node_discover <filter> : discovers nodes based on their type nd
|
|
2646
3104
|
Contacts
|
|
2647
3105
|
contacts / list : gets contact list lc
|
|
2648
3106
|
reload_contacts : force reloading all contacts rc
|
|
@@ -2661,7 +3119,7 @@ def command_help():
|
|
|
2661
3119
|
req_mma <ct> : requests min/max/avg for a sensor rm
|
|
2662
3120
|
req_acl <ct> : requests access control list for sensor
|
|
2663
3121
|
pending_contacts : show pending contacts
|
|
2664
|
-
add_pending <
|
|
3122
|
+
add_pending <pending> : manually add pending contact
|
|
2665
3123
|
flush_pending : flush pending contact list
|
|
2666
3124
|
Repeaters
|
|
2667
3125
|
login <name> <pwd> : log into a node (rep) with given pwd l
|
|
@@ -2696,6 +3154,43 @@ def usage () :
|
|
|
2696
3154
|
Available Commands and shorcuts (can be chained) :""")
|
|
2697
3155
|
command_help()
|
|
2698
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
|
+
|
|
2699
3194
|
async def main(argv):
|
|
2700
3195
|
""" Do the job """
|
|
2701
3196
|
json_output = JSON
|
|
@@ -2881,10 +3376,12 @@ async def main(argv):
|
|
|
2881
3376
|
handle_message.mc = mc # connect meshcore to handle_message
|
|
2882
3377
|
handle_advert.mc = mc
|
|
2883
3378
|
handle_path_update.mc = mc
|
|
3379
|
+
handle_log_rx.mc = mc
|
|
2884
3380
|
|
|
2885
3381
|
mc.subscribe(EventType.ADVERTISEMENT, handle_advert)
|
|
2886
3382
|
mc.subscribe(EventType.PATH_UPDATE, handle_path_update)
|
|
2887
3383
|
mc.subscribe(EventType.NEW_CONTACT, handle_new_contact)
|
|
3384
|
+
mc.subscribe(EventType.RX_LOG_DATA, handle_log_rx)
|
|
2888
3385
|
|
|
2889
3386
|
mc.auto_update_contacts = True
|
|
2890
3387
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meshcore-cli
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.10
|
|
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
|
|
@@ -10,8 +10,9 @@ License-File: LICENSE
|
|
|
10
10
|
Classifier: Operating System :: OS Independent
|
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
|
12
12
|
Requires-Python: >=3.10
|
|
13
|
-
Requires-Dist: meshcore>=2.1.
|
|
13
|
+
Requires-Dist: meshcore>=2.1.23
|
|
14
14
|
Requires-Dist: prompt-toolkit>=3.0.50
|
|
15
|
+
Requires-Dist: pycryptodome
|
|
15
16
|
Requires-Dist: requests>=2.28.0
|
|
16
17
|
Description-Content-Type: text/markdown
|
|
17
18
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
meshcore_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
meshcore_cli/__main__.py,sha256=PfYgibmu2LEtC-OV7L1UgmvV3swJ5rQ4bbXHlwUFlgE,83
|
|
3
|
+
meshcore_cli/meshcore_cli.py,sha256=IH0LlN7z14UgiUMGRhJmzp055d9AJ1jvEKV3CrXBfCA,141831
|
|
4
|
+
meshcore_cli-1.2.10.dist-info/METADATA,sha256=p4qn8oBD1_Hlegrk4Gw4pQwd3_2_Mt5yDtX6pWt41DI,11658
|
|
5
|
+
meshcore_cli-1.2.10.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
6
|
+
meshcore_cli-1.2.10.dist-info/entry_points.txt,sha256=77V29Pyth11GteDk7tneBN3MMk8JI7bTlS-BGSmxCmI,103
|
|
7
|
+
meshcore_cli-1.2.10.dist-info/licenses/LICENSE,sha256=F9s987VtS0AKxW7LdB2EkLMkrdeERI7ICdLJR60A9M4,1066
|
|
8
|
+
meshcore_cli-1.2.10.dist-info/RECORD,,
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
meshcore_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
meshcore_cli/__main__.py,sha256=PfYgibmu2LEtC-OV7L1UgmvV3swJ5rQ4bbXHlwUFlgE,83
|
|
3
|
-
meshcore_cli/meshcore_cli.py,sha256=6bMf4qSfISXSragUj9Av6i9xB-hE2ZfGmOs0-8D6D58,120885
|
|
4
|
-
meshcore_cli-1.2.1.dist-info/METADATA,sha256=Xh4Vcnhnevq2ZdPInOP0wSp2V3eRVj41THOnXPj8Gzw,11629
|
|
5
|
-
meshcore_cli-1.2.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
6
|
-
meshcore_cli-1.2.1.dist-info/entry_points.txt,sha256=77V29Pyth11GteDk7tneBN3MMk8JI7bTlS-BGSmxCmI,103
|
|
7
|
-
meshcore_cli-1.2.1.dist-info/licenses/LICENSE,sha256=F9s987VtS0AKxW7LdB2EkLMkrdeERI7ICdLJR60A9M4,1066
|
|
8
|
-
meshcore_cli-1.2.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|