meshcore-cli 1.2.5__tar.gz → 1.2.11__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {meshcore_cli-1.2.5 → meshcore_cli-1.2.11}/PKG-INFO +2 -2
- meshcore_cli-1.2.11/PXL_20251107_184348511.jpg +0 -0
- {meshcore_cli-1.2.5 → meshcore_cli-1.2.11}/pyproject.toml +2 -2
- {meshcore_cli-1.2.5 → meshcore_cli-1.2.11}/src/meshcore_cli/meshcore_cli.py +402 -50
- {meshcore_cli-1.2.5 → meshcore_cli-1.2.11}/.gitignore +0 -0
- {meshcore_cli-1.2.5 → meshcore_cli-1.2.11}/LICENSE +0 -0
- {meshcore_cli-1.2.5 → meshcore_cli-1.2.11}/README.md +0 -0
- {meshcore_cli-1.2.5 → meshcore_cli-1.2.11}/flake.lock +0 -0
- {meshcore_cli-1.2.5 → meshcore_cli-1.2.11}/flake.nix +0 -0
- {meshcore_cli-1.2.5 → meshcore_cli-1.2.11}/src/meshcore_cli/__init__.py +0 -0
- {meshcore_cli-1.2.5 → meshcore_cli-1.2.11}/src/meshcore_cli/__main__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meshcore-cli
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.11
|
|
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,7 +10,7 @@ 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
15
|
Requires-Dist: pycryptodome
|
|
16
16
|
Requires-Dist: requests>=2.28.0
|
|
Binary file
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "meshcore-cli"
|
|
7
|
-
version = "1.2.
|
|
7
|
+
version = "1.2.11"
|
|
8
8
|
authors = [
|
|
9
9
|
{ name="Florent de Lamotte", email="florent@frizoncorrea.fr" },
|
|
10
10
|
]
|
|
@@ -17,7 +17,7 @@ classifiers = [
|
|
|
17
17
|
]
|
|
18
18
|
license = "MIT"
|
|
19
19
|
license-files = ["LICEN[CS]E*"]
|
|
20
|
-
dependencies = [ "meshcore >= 2.1.
|
|
20
|
+
dependencies = [ "meshcore >= 2.1.23", "prompt_toolkit >= 3.0.50", "requests >= 2.28.0", "pycryptodome" ]
|
|
21
21
|
|
|
22
22
|
[project.urls]
|
|
23
23
|
Homepage = "https://github.com/fdlamotte/meshcore-cli"
|
|
@@ -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,7 +24,6 @@ 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
27
|
from Crypto.Cipher import AES
|
|
29
28
|
from Crypto.Hash import HMAC, SHA256
|
|
30
29
|
|
|
@@ -33,7 +32,7 @@ import re
|
|
|
33
32
|
from meshcore import MeshCore, EventType, logger
|
|
34
33
|
|
|
35
34
|
# Version
|
|
36
|
-
VERSION = "v1.2.
|
|
35
|
+
VERSION = "v1.2.11"
|
|
37
36
|
|
|
38
37
|
# default ble address is stored in a config file
|
|
39
38
|
MCCLI_CONFIG_DIR = str(Path.home()) + "/.config/meshcore/"
|
|
@@ -76,6 +75,15 @@ ANSI_BORANGE="\033[1;38;5;214m"
|
|
|
76
75
|
ANSI_YELLOW = "\033[0;33m"
|
|
77
76
|
ANSI_BYELLOW = "\033[1;33m"
|
|
78
77
|
|
|
78
|
+
#Unicode chars
|
|
79
|
+
# some possible symbols for prompts 🭬🬛🬗🭬🬛🬃🬗🭬🬛🬃🬗🬏🭀🭋🭨🮋
|
|
80
|
+
ARROW_TAIL = "🭨"
|
|
81
|
+
ARROW_HEAD = "🭬"
|
|
82
|
+
|
|
83
|
+
if platform.system() == 'Windows' or platform.system() == 'Darwin':
|
|
84
|
+
ARROW_TAIL = ""
|
|
85
|
+
ARROW_HEAD = " "
|
|
86
|
+
|
|
79
87
|
def escape_ansi(line):
|
|
80
88
|
ansi_escape = re.compile(r'(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]')
|
|
81
89
|
return ansi_escape.sub('', line)
|
|
@@ -208,15 +216,19 @@ async def handle_log_rx(event):
|
|
|
208
216
|
return
|
|
209
217
|
|
|
210
218
|
pkt = bytes().fromhex(event.payload["payload"])
|
|
219
|
+
pbuf = io.BytesIO(pkt)
|
|
220
|
+
header = pbuf.read(1)[0]
|
|
221
|
+
|
|
222
|
+
if header & ~1 == 0x14: # flood msg / channel
|
|
223
|
+
if handle_log_rx.channel_echoes:
|
|
224
|
+
if header & 1 == 0: # has transport code
|
|
225
|
+
pbuf.read(4) # discard transport code
|
|
226
|
+
path_len = pbuf.read(1)[0]
|
|
227
|
+
path = pbuf.read(path_len).hex()
|
|
228
|
+
chan_hash = pbuf.read(1).hex()
|
|
229
|
+
cipher_mac = pbuf.read(2)
|
|
230
|
+
msg = pbuf.read() # until the end of buffer
|
|
211
231
|
|
|
212
|
-
if handle_log_rx.channel_echoes:
|
|
213
|
-
if pkt[0] == 0x15:
|
|
214
|
-
chan_name = ""
|
|
215
|
-
path_len = pkt[1]
|
|
216
|
-
path = pkt[2:path_len+2].hex()
|
|
217
|
-
chan_hash = pkt[path_len+2:path_len+3].hex()
|
|
218
|
-
cipher_mac = pkt[path_len+3:path_len+5]
|
|
219
|
-
msg = pkt[path_len+5:]
|
|
220
232
|
channel = None
|
|
221
233
|
for c in await get_channels(mc):
|
|
222
234
|
if c["channel_hash"] == chan_hash : # validate against MAC
|
|
@@ -226,6 +238,8 @@ async def handle_log_rx(event):
|
|
|
226
238
|
channel = c
|
|
227
239
|
break
|
|
228
240
|
|
|
241
|
+
chan_name = ""
|
|
242
|
+
|
|
229
243
|
if channel is None :
|
|
230
244
|
if handle_log_rx.echo_unk_chans:
|
|
231
245
|
chan_name = chan_hash
|
|
@@ -235,7 +249,7 @@ async def handle_log_rx(event):
|
|
|
235
249
|
aes_key = bytes.fromhex(channel["channel_secret"])
|
|
236
250
|
cipher = AES.new(aes_key, AES.MODE_ECB)
|
|
237
251
|
message = cipher.decrypt(msg)[5:].decode("utf-8").strip("\x00")
|
|
238
|
-
|
|
252
|
+
|
|
239
253
|
if chan_name != "" :
|
|
240
254
|
width = os.get_terminal_size().columns
|
|
241
255
|
cars = width - 13 - 2 * path_len - len(chan_name) - 1
|
|
@@ -245,7 +259,7 @@ async def handle_log_rx(event):
|
|
|
245
259
|
print_above(txt)
|
|
246
260
|
else:
|
|
247
261
|
print(txt)
|
|
248
|
-
|
|
262
|
+
|
|
249
263
|
handle_log_rx.json_log_rx = False
|
|
250
264
|
handle_log_rx.channel_echoes = False
|
|
251
265
|
handle_log_rx.mc = None
|
|
@@ -322,7 +336,7 @@ async def log_message(mc, msg):
|
|
|
322
336
|
if msg["type"] == "PRIV" :
|
|
323
337
|
ct = mc.get_contact_by_key_prefix(msg['pubkey_prefix'])
|
|
324
338
|
if ct is None:
|
|
325
|
-
msg["name"] =
|
|
339
|
+
msg["name"] = msg["pubkey_prefix"]
|
|
326
340
|
else:
|
|
327
341
|
msg["name"] = ct["adv_name"]
|
|
328
342
|
elif msg["type"] == "CHAN" :
|
|
@@ -366,7 +380,7 @@ async def subscribe_to_msgs(mc, json_output=False, above=False):
|
|
|
366
380
|
class MyNestedCompleter(NestedCompleter):
|
|
367
381
|
def get_completions( self, document, complete_event):
|
|
368
382
|
txt = document.text_before_cursor.lstrip()
|
|
369
|
-
if not " " in txt:
|
|
383
|
+
if not " " in txt:
|
|
370
384
|
if txt != "" and txt[0] == "/" and txt.count("/") == 1:
|
|
371
385
|
opts = []
|
|
372
386
|
for k in self.options.keys():
|
|
@@ -445,6 +459,7 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
|
|
|
445
459
|
"share_contact" : contact_list,
|
|
446
460
|
"path": contact_list,
|
|
447
461
|
"disc_path" : contact_list,
|
|
462
|
+
"node_discover": {"all":None, "sens":None, "rep":None, "comp":None, "room":None, "cli":None},
|
|
448
463
|
"trace" : None,
|
|
449
464
|
"reset_path" : contact_list,
|
|
450
465
|
"change_path" : contact_list,
|
|
@@ -465,6 +480,9 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
|
|
|
465
480
|
"set_channel": None,
|
|
466
481
|
"get_channels": None,
|
|
467
482
|
"remove_channel": None,
|
|
483
|
+
"apply_to": None,
|
|
484
|
+
"at": None,
|
|
485
|
+
"scope": None,
|
|
468
486
|
"set" : {
|
|
469
487
|
"name" : None,
|
|
470
488
|
"pin" : None,
|
|
@@ -531,6 +549,8 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
|
|
|
531
549
|
|
|
532
550
|
contact_completion_list = {
|
|
533
551
|
"contact_info": None,
|
|
552
|
+
"contact_name": None,
|
|
553
|
+
"contact_lastmod": None,
|
|
534
554
|
"export_contact" : None,
|
|
535
555
|
"share_contact" : None,
|
|
536
556
|
"upload_contact" : None,
|
|
@@ -569,6 +589,7 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
|
|
|
569
589
|
"neighbors" : None,
|
|
570
590
|
"req_acl":None,
|
|
571
591
|
"setperm":contact_list,
|
|
592
|
+
"region" : {"get":None, "allowf": None, "denyf": None, "put": None, "remove": None, "save": None, "home": None},
|
|
572
593
|
"gps" : {"on":None,"off":None,"sync":None,"setloc":None,
|
|
573
594
|
"advert" : {"none": None, "share": None, "prefs": None},
|
|
574
595
|
},
|
|
@@ -652,7 +673,7 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
|
|
|
652
673
|
slash_root_completion_list = {}
|
|
653
674
|
for k,v in root_completion_list.items():
|
|
654
675
|
slash_root_completion_list["/"+k]=v
|
|
655
|
-
|
|
676
|
+
|
|
656
677
|
completion_list.update(slash_root_completion_list)
|
|
657
678
|
|
|
658
679
|
slash_contacts_completion_list = {}
|
|
@@ -697,6 +718,9 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
697
718
|
contact = to
|
|
698
719
|
prev_contact = None
|
|
699
720
|
|
|
721
|
+
scope = await set_scope(mc, "*")
|
|
722
|
+
prev_scope = scope
|
|
723
|
+
|
|
700
724
|
await get_contacts(mc, anim=True)
|
|
701
725
|
await get_channels(mc, anim=True)
|
|
702
726
|
await subscribe_to_msgs(mc, above=True)
|
|
@@ -735,6 +759,9 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
735
759
|
|
|
736
760
|
last_ack = True
|
|
737
761
|
while True:
|
|
762
|
+
# reset scope (if changed)
|
|
763
|
+
scope = await set_scope(mc, scope)
|
|
764
|
+
|
|
738
765
|
color = process_event_message.color
|
|
739
766
|
classic = interactive_loop.classic or not color
|
|
740
767
|
print_name = interactive_loop.print_name
|
|
@@ -744,14 +771,17 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
744
771
|
else:
|
|
745
772
|
prompt = f"{ANSI_INVERT}"
|
|
746
773
|
|
|
747
|
-
# some possible symbols for prompts 🭬🬛🬗🭬🬛🬃🬗🭬🬛🬃🬗🬏🭀🭋🭨🮋
|
|
748
774
|
if print_name or contact is None :
|
|
749
|
-
|
|
775
|
+
if color:
|
|
776
|
+
prompt = prompt + f"{ANSI_BGRAY}"
|
|
750
777
|
prompt = prompt + f"{mc.self_info['name']}"
|
|
778
|
+
if contact is None: # display scope
|
|
779
|
+
if not scope is None:
|
|
780
|
+
prompt = prompt + f"|{scope}"
|
|
751
781
|
if classic :
|
|
752
|
-
prompt = prompt + "
|
|
782
|
+
prompt = prompt + "> "
|
|
753
783
|
else :
|
|
754
|
-
prompt = prompt + f"{ANSI_NORMAL}
|
|
784
|
+
prompt = prompt + f"{ANSI_NORMAL}{ARROW_HEAD}{ANSI_INVERT}"
|
|
755
785
|
|
|
756
786
|
if not contact is None :
|
|
757
787
|
if not last_ack:
|
|
@@ -772,13 +802,24 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
772
802
|
prompt = prompt + f"{ANSI_INVERT}"
|
|
773
803
|
|
|
774
804
|
if print_name and not classic :
|
|
775
|
-
prompt = prompt + f"{ANSI_NORMAL}
|
|
805
|
+
prompt = prompt + f"{ANSI_NORMAL}{ARROW_TAIL}{ANSI_INVERT}"
|
|
776
806
|
|
|
777
807
|
prompt = prompt + f"{contact['adv_name']}"
|
|
808
|
+
if contact["type"] == 0 or contact["out_path_len"]==-1:
|
|
809
|
+
if scope is None:
|
|
810
|
+
prompt = prompt + f"|*"
|
|
811
|
+
else:
|
|
812
|
+
prompt = prompt + f"|{scope}"
|
|
813
|
+
else: # display path to dest or 0 if 0 hop
|
|
814
|
+
if contact["out_path_len"] == 0:
|
|
815
|
+
prompt = prompt + f"|0"
|
|
816
|
+
else:
|
|
817
|
+
prompt = prompt + "|" + contact["out_path"]
|
|
818
|
+
|
|
778
819
|
if classic :
|
|
779
|
-
prompt = prompt + f"{ANSI_NORMAL}
|
|
820
|
+
prompt = prompt + f"{ANSI_NORMAL}> "
|
|
780
821
|
else:
|
|
781
|
-
prompt = prompt + f"{ANSI_NORMAL}
|
|
822
|
+
prompt = prompt + f"{ANSI_NORMAL}{ARROW_HEAD}"
|
|
782
823
|
|
|
783
824
|
prompt = prompt + f"{ANSI_END}"
|
|
784
825
|
|
|
@@ -799,9 +840,14 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
799
840
|
completer=completer,
|
|
800
841
|
key_bindings=bindings)
|
|
801
842
|
|
|
843
|
+
line = line.strip()
|
|
844
|
+
|
|
802
845
|
if line == "" : # blank line
|
|
803
846
|
pass
|
|
804
847
|
|
|
848
|
+
elif line.startswith("?") :
|
|
849
|
+
get_help_for(line[1:], context="chat")
|
|
850
|
+
|
|
805
851
|
# raw meshcli command as on command line
|
|
806
852
|
elif line.startswith("$") :
|
|
807
853
|
try :
|
|
@@ -810,18 +856,41 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
810
856
|
except ValueError:
|
|
811
857
|
logger.error("Error parsing line {line[1:]}")
|
|
812
858
|
|
|
859
|
+
elif line.startswith("/scope") or\
|
|
860
|
+
line.startswith("scope") and contact is None:
|
|
861
|
+
if not scope is None:
|
|
862
|
+
prev_scope = scope
|
|
863
|
+
try:
|
|
864
|
+
newscope = line.split(" ", 1)[1]
|
|
865
|
+
scope = await set_scope(mc, newscope)
|
|
866
|
+
except IndexError:
|
|
867
|
+
print(scope)
|
|
868
|
+
|
|
869
|
+
elif contact is None and (line.startswith("apply_to ") or line.startswith("at ")) or\
|
|
870
|
+
line.startswith("/apply_to ") or line.startswith("/at ") :
|
|
871
|
+
try:
|
|
872
|
+
await apply_command_to_contacts(mc, line.split(" ",2)[1], line.split(" ",2)[2])
|
|
873
|
+
except IndexError:
|
|
874
|
+
logger.error(f"Error with apply_to command parameters")
|
|
875
|
+
|
|
813
876
|
elif line.startswith("/") :
|
|
814
877
|
path = line.split(" ", 1)[0]
|
|
815
878
|
if path.count("/") == 1:
|
|
816
879
|
args = line[1:].split(" ")
|
|
817
|
-
|
|
880
|
+
dest = args[0]
|
|
881
|
+
dest_scope = None
|
|
882
|
+
if "%" in dest :
|
|
883
|
+
dest_scope = dest.split("%")[-1]
|
|
884
|
+
dest = dest[:-len(dest_scope)-1]
|
|
885
|
+
await set_scope (mc, dest_scope)
|
|
886
|
+
tct = mc.get_contact_by_name(dest)
|
|
818
887
|
if len(args)>1 and not tct is None: # a contact, send a message
|
|
819
888
|
if tct["type"] == 1 or tct["type"] == 3: # client or room
|
|
820
889
|
last_ack = await msg_ack(mc, tct, line.split(" ", 1)[1])
|
|
821
890
|
else:
|
|
822
891
|
print("Can only send msg to chan, client or room")
|
|
823
892
|
else :
|
|
824
|
-
ch = await get_channel_by_name(mc,
|
|
893
|
+
ch = await get_channel_by_name(mc, dest)
|
|
825
894
|
if len(args)>1 and not ch is None: # a channel, send message
|
|
826
895
|
await send_chan_msg(mc, ch["channel_idx"], line.split(" ", 1)[1])
|
|
827
896
|
else :
|
|
@@ -832,6 +901,11 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
832
901
|
else:
|
|
833
902
|
cmdline = line[1:].split("/",1)[1]
|
|
834
903
|
contact_name = path[1:].split("/",1)[0]
|
|
904
|
+
dest_scope = None
|
|
905
|
+
if "%" in contact_name:
|
|
906
|
+
dest_scope = contact_name.split("%")[-1]
|
|
907
|
+
contact_name = contact_name[:-len(dest_scope)-1]
|
|
908
|
+
await set_scope (mc, dest_scope)
|
|
835
909
|
tct = mc.get_contact_by_name(contact_name)
|
|
836
910
|
if tct is None:
|
|
837
911
|
print(f"{contact_name} is not a contact")
|
|
@@ -847,6 +921,10 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
847
921
|
dest = line[3:]
|
|
848
922
|
if dest.startswith("\"") or dest.startswith("\'") : # if name starts with a quote
|
|
849
923
|
dest = shlex.split(dest)[0] # use shlex.split to get contact name between quotes
|
|
924
|
+
dest_scope = None
|
|
925
|
+
if '%' in dest and scope!=None :
|
|
926
|
+
dest_scope = dest.split("%")[-1]
|
|
927
|
+
dest = dest[:-len(dest_scope)-1]
|
|
850
928
|
nc = mc.get_contact_by_name(dest)
|
|
851
929
|
if nc is None:
|
|
852
930
|
if dest == "public" :
|
|
@@ -860,6 +938,8 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
860
938
|
nc["adv_name"] = mc.channels[dest]["channel_name"]
|
|
861
939
|
elif dest == ".." : # previous recipient
|
|
862
940
|
nc = prev_contact
|
|
941
|
+
if dest_scope is None and not prev_scope is None:
|
|
942
|
+
dest_scope = prev_scope
|
|
863
943
|
elif dest == "~" or dest == "/" or dest == mc.self_info['name']:
|
|
864
944
|
nc = None
|
|
865
945
|
elif dest == "!" :
|
|
@@ -877,6 +957,12 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
|
|
|
877
957
|
last_ack = True
|
|
878
958
|
prev_contact = contact
|
|
879
959
|
contact = nc
|
|
960
|
+
if dest_scope is None:
|
|
961
|
+
dest_scope = scope
|
|
962
|
+
if not scope is None and dest_scope != scope:
|
|
963
|
+
prev_scope = scope
|
|
964
|
+
if not dest_scope is None:
|
|
965
|
+
scope = await set_scope(mc, dest_scope)
|
|
880
966
|
|
|
881
967
|
elif line == "to" :
|
|
882
968
|
if contact is None :
|
|
@@ -956,6 +1042,12 @@ async def process_contact_chat_line(mc, contact, line):
|
|
|
956
1042
|
if contact["type"] == 0:
|
|
957
1043
|
return False
|
|
958
1044
|
|
|
1045
|
+
# if one element in line (most cases) strip the scope and apply it
|
|
1046
|
+
if not " " in line and "%" in line:
|
|
1047
|
+
dest_scope = line.split("%")[-1]
|
|
1048
|
+
line = line[:-len(dest_scope)-1]
|
|
1049
|
+
await set_scope (mc, dest_scope)
|
|
1050
|
+
|
|
959
1051
|
if line.startswith(":") : # : will send a command to current recipient
|
|
960
1052
|
args=["cmd", contact['adv_name'], line[1:]]
|
|
961
1053
|
await process_cmds(mc, args)
|
|
@@ -966,6 +1058,23 @@ async def process_contact_chat_line(mc, contact, line):
|
|
|
966
1058
|
await process_cmds(mc, args)
|
|
967
1059
|
return True
|
|
968
1060
|
|
|
1061
|
+
if line.startswith("contact_name") or line.startswith("cn"):
|
|
1062
|
+
print(contact['adv_name'],end="")
|
|
1063
|
+
if " " in line:
|
|
1064
|
+
print(" ", end="", flush=True)
|
|
1065
|
+
secline = line.split(" ", 1)[1]
|
|
1066
|
+
await process_contact_chat_line(mc, contact, secline)
|
|
1067
|
+
else:
|
|
1068
|
+
print("")
|
|
1069
|
+
return True
|
|
1070
|
+
|
|
1071
|
+
if line == "contact_lastmod":
|
|
1072
|
+
timestamp = contact["lastmod"]
|
|
1073
|
+
print(f"{contact['adv_name']} updated"
|
|
1074
|
+
f" {datetime.datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d at %H:%M:%S')}"
|
|
1075
|
+
f" ({timestamp})")
|
|
1076
|
+
return True
|
|
1077
|
+
|
|
969
1078
|
# commands that take contact as second arg will be sent to recipient
|
|
970
1079
|
if line == "sc" or line == "share_contact" or\
|
|
971
1080
|
line == "ec" or line == "export_contact" or\
|
|
@@ -1053,15 +1162,21 @@ async def process_contact_chat_line(mc, contact, line):
|
|
|
1053
1162
|
return True
|
|
1054
1163
|
|
|
1055
1164
|
# same but for commands with a parameter
|
|
1056
|
-
if
|
|
1057
|
-
line.startswith("cp ") or line.startswith("change_path ") or\
|
|
1058
|
-
line.startswith("cf ") or line.startswith("change_flags ") or\
|
|
1059
|
-
line.startswith("req_binary ") or\
|
|
1060
|
-
line.startswith("login ") :
|
|
1165
|
+
if " " in line:
|
|
1061
1166
|
cmds = line.split(" ", 1)
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1167
|
+
if "%" in cmds[0]:
|
|
1168
|
+
dest_scope = cmds[0].split("%")[-1]
|
|
1169
|
+
cmds[0] = cmds[0][:-len(dest_scope)-1]
|
|
1170
|
+
await set_scope(mc, dest_scope)
|
|
1171
|
+
|
|
1172
|
+
if cmds[0] == "cmd" or cmds[0] == "msg" or\
|
|
1173
|
+
cmds[0] == "cp" or cmds[0] == "change_path" or\
|
|
1174
|
+
cmds[0] == "cf" or cmds[0] == "change_flags" or\
|
|
1175
|
+
cmds[0] == "req_binary" or\
|
|
1176
|
+
cmds[0] == "login" :
|
|
1177
|
+
args = [cmds[0], contact['adv_name'], cmds[1]]
|
|
1178
|
+
await process_cmds(mc, args)
|
|
1179
|
+
return True
|
|
1065
1180
|
|
|
1066
1181
|
if line == "login": # use stored password or prompt for it
|
|
1067
1182
|
password_file = ""
|
|
@@ -1085,7 +1200,7 @@ async def process_contact_chat_line(mc, contact, line):
|
|
|
1085
1200
|
|
|
1086
1201
|
if password == "":
|
|
1087
1202
|
try:
|
|
1088
|
-
sess = PromptSession("Password: ", is_password=True)
|
|
1203
|
+
sess = PromptSession(f"Password for {contact['adv_name']}: ", is_password=True)
|
|
1089
1204
|
password = await sess.prompt_async()
|
|
1090
1205
|
except EOFError:
|
|
1091
1206
|
logger.info("Canceled")
|
|
@@ -1124,6 +1239,89 @@ async def process_contact_chat_line(mc, contact, line):
|
|
|
1124
1239
|
|
|
1125
1240
|
return False
|
|
1126
1241
|
|
|
1242
|
+
async def apply_command_to_contacts(mc, contact_filter, line):
|
|
1243
|
+
upd_before = None
|
|
1244
|
+
upd_after = None
|
|
1245
|
+
contact_type = None
|
|
1246
|
+
min_hops = None
|
|
1247
|
+
max_hops = None
|
|
1248
|
+
|
|
1249
|
+
await mc.ensure_contacts()
|
|
1250
|
+
|
|
1251
|
+
filters = contact_filter.split(",")
|
|
1252
|
+
for f in filters:
|
|
1253
|
+
if f == "all":
|
|
1254
|
+
pass
|
|
1255
|
+
elif f[0] == "u": #updated
|
|
1256
|
+
val_str = f[2:]
|
|
1257
|
+
t = time.time()
|
|
1258
|
+
if val_str[-1] == "d": # value in days
|
|
1259
|
+
t = t - float(val_str[0:-1]) * 86400
|
|
1260
|
+
elif val_str[-1] == "h": # value in hours
|
|
1261
|
+
t = t - float(val_str[0:-1]) * 3600
|
|
1262
|
+
elif val_str[-1] == "m": # value in minutes
|
|
1263
|
+
t = t - float(val_str[0:-1]) * 60
|
|
1264
|
+
else:
|
|
1265
|
+
t = int(val_str)
|
|
1266
|
+
if f[1] == "<": #before
|
|
1267
|
+
upd_before = t
|
|
1268
|
+
elif f[1] == ">":
|
|
1269
|
+
upd_after = t
|
|
1270
|
+
else:
|
|
1271
|
+
logger.error(f"Time filter can only be < or >")
|
|
1272
|
+
return
|
|
1273
|
+
elif f[0] == "t": # type
|
|
1274
|
+
if f[1] == "=":
|
|
1275
|
+
contact_type = int(f[2:])
|
|
1276
|
+
else:
|
|
1277
|
+
logger.error(f"Type can only be equals to a value")
|
|
1278
|
+
return
|
|
1279
|
+
elif f[0] == "d": # direct
|
|
1280
|
+
min_hops=0
|
|
1281
|
+
elif f[0] == "f": # flood
|
|
1282
|
+
max_hops=-1
|
|
1283
|
+
elif f[0] == "h": # hop number
|
|
1284
|
+
if f[1] == ">":
|
|
1285
|
+
min_hops = int(f[2:])+1
|
|
1286
|
+
elif f[1] == "<":
|
|
1287
|
+
max_hops = int(f[2:])-1
|
|
1288
|
+
elif f[1] == "=":
|
|
1289
|
+
min_hops = int(f[2:])
|
|
1290
|
+
max_hops = int(f[2:])
|
|
1291
|
+
else:
|
|
1292
|
+
logger.error(f"Unknown filter {f}")
|
|
1293
|
+
return
|
|
1294
|
+
|
|
1295
|
+
for c in dict(mc._contacts).items():
|
|
1296
|
+
contact = c[1]
|
|
1297
|
+
if (contact_type is None or contact["type"] == contact_type) and\
|
|
1298
|
+
(upd_before is None or contact["lastmod"] < upd_before) and\
|
|
1299
|
+
(upd_after is None or contact["lastmod"] > upd_after) and\
|
|
1300
|
+
(min_hops is None or contact["out_path_len"] >= min_hops) and\
|
|
1301
|
+
(max_hops is None or contact["out_path_len"] <= max_hops):
|
|
1302
|
+
if await process_contact_chat_line(mc, contact, line):
|
|
1303
|
+
pass
|
|
1304
|
+
|
|
1305
|
+
elif line == "remove_contact":
|
|
1306
|
+
args = [line, contact['adv_name']]
|
|
1307
|
+
await process_cmds(mc, args)
|
|
1308
|
+
|
|
1309
|
+
elif line.startswith("send") or line.startswith("\"") :
|
|
1310
|
+
if line.startswith("send") :
|
|
1311
|
+
line = line[5:]
|
|
1312
|
+
if line.startswith("\"") :
|
|
1313
|
+
line = line[1:]
|
|
1314
|
+
await msg_ack(mc, contact, line)
|
|
1315
|
+
|
|
1316
|
+
elif contact["type"] == 2 or\
|
|
1317
|
+
contact["type"] == 3 or\
|
|
1318
|
+
contact["type"] == 4 : # repeater, room, sensor send cmd
|
|
1319
|
+
await process_cmds(mc, ["cmd", contact["adv_name"], line])
|
|
1320
|
+
# wait for a reply from cmd
|
|
1321
|
+
await mc.wait_for_event(EventType.MESSAGES_WAITING, timeout=7)
|
|
1322
|
+
|
|
1323
|
+
else:
|
|
1324
|
+
logger.error(f"Can't send {line} to {contact['adv_name']}")
|
|
1127
1325
|
|
|
1128
1326
|
async def send_cmd (mc, contact, cmd) :
|
|
1129
1327
|
res = await mc.commands.send_cmd(contact, cmd)
|
|
@@ -1167,7 +1365,7 @@ async def send_msg (mc, contact, msg) :
|
|
|
1167
1365
|
|
|
1168
1366
|
async def msg_ack (mc, contact, msg) :
|
|
1169
1367
|
timeout = 0 if not 'timeout' in contact else contact['timeout']
|
|
1170
|
-
res = await mc.commands.send_msg_with_retry(contact, msg,
|
|
1368
|
+
res = await mc.commands.send_msg_with_retry(contact, msg,
|
|
1171
1369
|
max_attempts=msg_ack.max_attempts,
|
|
1172
1370
|
flood_after=msg_ack.flood_after,
|
|
1173
1371
|
max_flood_attempts=msg_ack.max_flood_attempts,
|
|
@@ -1187,6 +1385,28 @@ msg_ack.max_attempts=3
|
|
|
1187
1385
|
msg_ack.flood_after=2
|
|
1188
1386
|
msg_ack.max_flood_attempts=1
|
|
1189
1387
|
|
|
1388
|
+
async def set_scope (mc, scope) :
|
|
1389
|
+
if not set_scope.has_scope:
|
|
1390
|
+
return None
|
|
1391
|
+
|
|
1392
|
+
if scope == "None" or scope == "0" or scope == "clear" or scope == "":
|
|
1393
|
+
scope = "*"
|
|
1394
|
+
|
|
1395
|
+
if set_scope.current_scope == scope:
|
|
1396
|
+
return scope
|
|
1397
|
+
|
|
1398
|
+
res = await mc.commands.set_flood_scope(scope)
|
|
1399
|
+
if res is None or res.type == EventType.ERROR:
|
|
1400
|
+
if not res is None and res.payload["error_code"] == 1: #unsupported
|
|
1401
|
+
set_scope.has_scope = False
|
|
1402
|
+
return None
|
|
1403
|
+
|
|
1404
|
+
set_scope.current_scope = scope
|
|
1405
|
+
|
|
1406
|
+
return scope
|
|
1407
|
+
set_scope.has_scope = True
|
|
1408
|
+
set_scope.current_scope = None
|
|
1409
|
+
|
|
1190
1410
|
async def get_channel (mc, chan) :
|
|
1191
1411
|
if not chan.isnumeric():
|
|
1192
1412
|
return await get_channel_by_name(mc, chan)
|
|
@@ -1223,7 +1443,7 @@ async def set_channel (mc, chan, name, key=None):
|
|
|
1223
1443
|
return None
|
|
1224
1444
|
|
|
1225
1445
|
info = res.payload
|
|
1226
|
-
info["channel_hash"] =
|
|
1446
|
+
info["channel_hash"] = SHA256.new(info["channel_secret"]).hexdigest()[0:2]
|
|
1227
1447
|
info["channel_secret"] = info["channel_secret"].hex()
|
|
1228
1448
|
|
|
1229
1449
|
if hasattr(mc,'channels') :
|
|
@@ -1312,7 +1532,7 @@ async def get_channels (mc, anim=False) :
|
|
|
1312
1532
|
if res.type == EventType.ERROR:
|
|
1313
1533
|
break
|
|
1314
1534
|
info = res.payload
|
|
1315
|
-
info["channel_hash"] =
|
|
1535
|
+
info["channel_hash"] = SHA256.new(info["channel_secret"]).hexdigest()[0:2]
|
|
1316
1536
|
info["channel_secret"] = info["channel_secret"].hex()
|
|
1317
1537
|
mc.channels.append(info)
|
|
1318
1538
|
ch = ch + 1
|
|
@@ -1393,8 +1613,14 @@ async def print_disc_trace_to (mc, contact):
|
|
|
1393
1613
|
|
|
1394
1614
|
async def next_cmd(mc, cmds, json_output=False):
|
|
1395
1615
|
""" process next command """
|
|
1616
|
+
global ARROW_TAIL, ARROW_HEAD
|
|
1396
1617
|
try :
|
|
1397
1618
|
argnum = 0
|
|
1619
|
+
|
|
1620
|
+
if cmds[0].startswith("?") : # get some help
|
|
1621
|
+
get_help_for(cmds[0][1:], context="line")
|
|
1622
|
+
return cmds[argnum+1:]
|
|
1623
|
+
|
|
1398
1624
|
if cmds[0].startswith(".") : # override json_output
|
|
1399
1625
|
json_output = True
|
|
1400
1626
|
cmd = cmds[0][1:]
|
|
@@ -1485,6 +1711,10 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
1485
1711
|
else:
|
|
1486
1712
|
print("Time set")
|
|
1487
1713
|
|
|
1714
|
+
case "apply_to"|"at":
|
|
1715
|
+
argnum = 2
|
|
1716
|
+
await apply_command_to_contacts(mc, cmds[1], cmds[2])
|
|
1717
|
+
|
|
1488
1718
|
case "set":
|
|
1489
1719
|
argnum = 2
|
|
1490
1720
|
match cmds[1]:
|
|
@@ -1517,6 +1747,10 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
1517
1747
|
interactive_loop.classic = (cmds[2] == "on")
|
|
1518
1748
|
if json_output :
|
|
1519
1749
|
print(json.dumps({"cmd" : cmds[1], "param" : cmds[2]}))
|
|
1750
|
+
case "arrow_tail":
|
|
1751
|
+
ARROW_TAIL = cmds[2]
|
|
1752
|
+
case "arrow_head":
|
|
1753
|
+
ARROW_HEAD = cmds[2]
|
|
1520
1754
|
case "color" :
|
|
1521
1755
|
process_event_message.color = (cmds[2] == "on")
|
|
1522
1756
|
if json_output :
|
|
@@ -1979,6 +2213,12 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
1979
2213
|
if res is None:
|
|
1980
2214
|
print("Error setting channel")
|
|
1981
2215
|
|
|
2216
|
+
case "scope":
|
|
2217
|
+
argnum = 1
|
|
2218
|
+
res = await set_scope(mc, cmds[1])
|
|
2219
|
+
if res is None:
|
|
2220
|
+
print(f"Error while setting scope")
|
|
2221
|
+
|
|
1982
2222
|
case "remove_channel":
|
|
1983
2223
|
argnum = 1
|
|
1984
2224
|
res = await set_channel(mc, cmds[1], "", bytes.fromhex(16*"00"))
|
|
@@ -2045,7 +2285,7 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
2045
2285
|
argnum = 2
|
|
2046
2286
|
dest = None
|
|
2047
2287
|
|
|
2048
|
-
if len(cmds[1]) == 12: # possibly an hex prefix
|
|
2288
|
+
if len(cmds[1]) == 12: # possibly an hex prefix
|
|
2049
2289
|
try:
|
|
2050
2290
|
dest = bytes.fromhex(cmds[1])
|
|
2051
2291
|
except ValueError:
|
|
@@ -2100,7 +2340,8 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
2100
2340
|
if json_output:
|
|
2101
2341
|
print(json.dumps(ev.payload, indent=2))
|
|
2102
2342
|
else :
|
|
2103
|
-
|
|
2343
|
+
color = process_event_message.color
|
|
2344
|
+
classic = interactive_loop.classic or not color
|
|
2104
2345
|
print("]",end="")
|
|
2105
2346
|
for t in ev.payload["path"]:
|
|
2106
2347
|
if classic :
|
|
@@ -2108,18 +2349,20 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
2108
2349
|
else:
|
|
2109
2350
|
print(f" {ANSI_INVERT}", end="")
|
|
2110
2351
|
snr = t['snr']
|
|
2111
|
-
if
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2352
|
+
if color:
|
|
2353
|
+
if snr >= 10 :
|
|
2354
|
+
print(ANSI_BGREEN, end="")
|
|
2355
|
+
elif snr <= 0:
|
|
2356
|
+
print(ANSI_BRED, end="")
|
|
2357
|
+
else :
|
|
2358
|
+
print(ANSI_BGRAY, end="")
|
|
2117
2359
|
print(f"{snr:.2f}",end="")
|
|
2118
2360
|
if classic :
|
|
2119
2361
|
print("→",end="")
|
|
2120
2362
|
else :
|
|
2121
|
-
print(f"{ANSI_NORMAL}
|
|
2122
|
-
|
|
2363
|
+
print(f"{ANSI_NORMAL}{ARROW_HEAD}",end="")
|
|
2364
|
+
if color:
|
|
2365
|
+
print(ANSI_END, end="")
|
|
2123
2366
|
if "hash" in t:
|
|
2124
2367
|
print(f"[{t['hash']}]",end="")
|
|
2125
2368
|
else:
|
|
@@ -2243,6 +2486,71 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
2243
2486
|
inp = inp if inp != "" else "direct"
|
|
2244
2487
|
print(f"Path for {contact['adv_name']}: out {outp}, in {inp}")
|
|
2245
2488
|
|
|
2489
|
+
case "node_discover"|"nd" :
|
|
2490
|
+
argnum = 1
|
|
2491
|
+
prefix_only = True
|
|
2492
|
+
|
|
2493
|
+
if len(cmds) == 1:
|
|
2494
|
+
argnum = 0
|
|
2495
|
+
types = 0xFF
|
|
2496
|
+
else:
|
|
2497
|
+
try: # try to decode type as int
|
|
2498
|
+
types = int(cmds[1])
|
|
2499
|
+
except ValueError:
|
|
2500
|
+
if "all" in cmds[1]:
|
|
2501
|
+
types = 0xFF
|
|
2502
|
+
else :
|
|
2503
|
+
types = 0
|
|
2504
|
+
if "rep" in cmds[1] or "rpt" in cmds[1]:
|
|
2505
|
+
types = types | 4
|
|
2506
|
+
if "cli" in cmds[1] or "comp" in cmds[1]:
|
|
2507
|
+
types = types | 2
|
|
2508
|
+
if "room" in cmds[1]:
|
|
2509
|
+
types = types | 8
|
|
2510
|
+
if "sens" in cmds[1]:
|
|
2511
|
+
types = types | 16
|
|
2512
|
+
|
|
2513
|
+
if "full" in cmds[1]:
|
|
2514
|
+
prefix_only = False
|
|
2515
|
+
|
|
2516
|
+
res = await mc.commands.send_node_discover_req(types, prefix_only=prefix_only)
|
|
2517
|
+
if res is None or res.type == EventType.ERROR:
|
|
2518
|
+
print("Error sending discover request")
|
|
2519
|
+
else:
|
|
2520
|
+
exp_tag = res.payload["tag"].to_bytes(4, "little").hex()
|
|
2521
|
+
dn = []
|
|
2522
|
+
while True:
|
|
2523
|
+
r = await mc.wait_for_event(
|
|
2524
|
+
EventType.DISCOVER_RESPONSE,
|
|
2525
|
+
attribute_filters={"tag":exp_tag},
|
|
2526
|
+
timeout = 5
|
|
2527
|
+
)
|
|
2528
|
+
if r is None or r.type == EventType.ERROR:
|
|
2529
|
+
break
|
|
2530
|
+
else:
|
|
2531
|
+
dn.append(r.payload)
|
|
2532
|
+
|
|
2533
|
+
if json_output:
|
|
2534
|
+
print(json.dumps(dn))
|
|
2535
|
+
else:
|
|
2536
|
+
await mc.ensure_contacts()
|
|
2537
|
+
print(f"Discovered {len(dn)} nodes:")
|
|
2538
|
+
for n in dn:
|
|
2539
|
+
name = f"{n['pubkey'][0:2]} {mc.get_contact_by_key_prefix(n['pubkey'])['adv_name']}"
|
|
2540
|
+
if name is None:
|
|
2541
|
+
name = n["pubkey"][0:16]
|
|
2542
|
+
type = f"t:{n['node_type']}"
|
|
2543
|
+
if n['node_type'] == 1:
|
|
2544
|
+
type = "CLI"
|
|
2545
|
+
elif n['node_type'] == 2:
|
|
2546
|
+
type = "REP"
|
|
2547
|
+
elif n['node_type'] == 3:
|
|
2548
|
+
type = "ROOM"
|
|
2549
|
+
elif n['node_type'] == 4:
|
|
2550
|
+
type = "SENS"
|
|
2551
|
+
|
|
2552
|
+
print(f" {name:16} {type:>4} SNR: {n['SNR_in']:6,.2f}->{n['SNR']:6,.2f} RSSI: ->{n['RSSI']:4}")
|
|
2553
|
+
|
|
2246
2554
|
case "req_btelemetry"|"rbt" :
|
|
2247
2555
|
argnum = 1
|
|
2248
2556
|
await mc.ensure_contacts()
|
|
@@ -2718,7 +3026,7 @@ async def next_cmd(mc, cmds, json_output=False):
|
|
|
2718
3026
|
await mc.ensure_contacts()
|
|
2719
3027
|
contact = mc.get_contact_by_name(cmds[0])
|
|
2720
3028
|
if contact is None:
|
|
2721
|
-
logger.error(f"Unknown command : {cmd}
|
|
3029
|
+
logger.error(f"Unknown command : {cmd}, {cmds} not executed ...")
|
|
2722
3030
|
return None
|
|
2723
3031
|
|
|
2724
3032
|
await interactive_loop(mc, to=contact)
|
|
@@ -2762,7 +3070,8 @@ def version():
|
|
|
2762
3070
|
print (f"meshcore-cli: command line interface to MeshCore companion radios {VERSION}")
|
|
2763
3071
|
|
|
2764
3072
|
def command_help():
|
|
2765
|
-
print("""
|
|
3073
|
+
print(""" ?<cmd> may give you some more help about cmd
|
|
3074
|
+
General commands
|
|
2766
3075
|
chat : enter the chat (interactive) mode
|
|
2767
3076
|
chat_to <ct> : enter chat with contact to
|
|
2768
3077
|
script <filename> : execute commands in filename
|
|
@@ -2773,6 +3082,7 @@ def command_help():
|
|
|
2773
3082
|
reboot : reboots node
|
|
2774
3083
|
sleep <secs> : sleeps for a given amount of secs s
|
|
2775
3084
|
wait_key : wait until user presses <Enter> wk
|
|
3085
|
+
apply_to <scope> <cmds>: sends cmds to contacts matching scope at
|
|
2776
3086
|
Messenging
|
|
2777
3087
|
msg <name> <msg> : send message to node by name m {
|
|
2778
3088
|
wait_ack : wait an ack wa }
|
|
@@ -2794,6 +3104,7 @@ def command_help():
|
|
|
2794
3104
|
time <epoch> : sets time to given epoch
|
|
2795
3105
|
clock : get current time
|
|
2796
3106
|
clock sync : sync device clock st
|
|
3107
|
+
node_discover <filter> : discovers nodes based on their type nd
|
|
2797
3108
|
Contacts
|
|
2798
3109
|
contacts / list : gets contact list lc
|
|
2799
3110
|
reload_contacts : force reloading all contacts rc
|
|
@@ -2835,6 +3146,7 @@ def usage () :
|
|
|
2835
3146
|
-D : debug
|
|
2836
3147
|
-S : scan for devices and show a selector
|
|
2837
3148
|
-l : list available ble/serial devices and exit
|
|
3149
|
+
-c <on/off> : disables most of color output if off
|
|
2838
3150
|
-T <timeout> : timeout for the ble scan (-S and -l) default 2s
|
|
2839
3151
|
-a <address> : specifies device address (can be a name)
|
|
2840
3152
|
-d <name> : filter meshcore devices with name or address
|
|
@@ -2847,6 +3159,43 @@ def usage () :
|
|
|
2847
3159
|
Available Commands and shorcuts (can be chained) :""")
|
|
2848
3160
|
command_help()
|
|
2849
3161
|
|
|
3162
|
+
def get_help_for (cmdname, context="line") :
|
|
3163
|
+
if cmdname == "apply_to" or cmdname == "at" :
|
|
3164
|
+
print("""apply_to <scope> <cmd> : applies cmd to contacts matching scope
|
|
3165
|
+
Scope acts like a filter with comma separated fields :
|
|
3166
|
+
- u, matches modification time < or > than a timestamp
|
|
3167
|
+
(can also be days hours or minutes ago if followed by d,h or m)
|
|
3168
|
+
- t, matches the type (1: client, 2: repeater, 3: room, 4: sensor)
|
|
3169
|
+
- h, matches number of hops
|
|
3170
|
+
- d, direct, similar to h>-1
|
|
3171
|
+
- f, flood, similar to h<0 or h=-1
|
|
3172
|
+
|
|
3173
|
+
Note: Some commands like contact_name (aka cn), reset_path (aka rp), forget_password (aka fp) can be chained.
|
|
3174
|
+
|
|
3175
|
+
Examples:
|
|
3176
|
+
# removes all clients that have not been updated in last 2 days
|
|
3177
|
+
at u<2d,t=1 remove_contact
|
|
3178
|
+
# gives traces to repeaters that have been updated in the last 24h and are direct
|
|
3179
|
+
at t=2,u>1d,d cn trace
|
|
3180
|
+
# tries to do flood login to all repeaters
|
|
3181
|
+
at t=2 rp login
|
|
3182
|
+
""")
|
|
3183
|
+
|
|
3184
|
+
if cmdname == "node_discover" or cmdname == "nd" :
|
|
3185
|
+
print("""node_discover <filter> : discovers 0-hop nodes and displays signal info
|
|
3186
|
+
|
|
3187
|
+
filter can be "all" for all types or nodes or a comma separated list consisting of :
|
|
3188
|
+
- cli or comp for companions
|
|
3189
|
+
- rep for repeaters
|
|
3190
|
+
- sens for sensors
|
|
3191
|
+
- room for chat rooms
|
|
3192
|
+
|
|
3193
|
+
nd can be used with no filter parameter ... !!! BEWARE WITH CHAINING !!!
|
|
3194
|
+
""")
|
|
3195
|
+
|
|
3196
|
+
else:
|
|
3197
|
+
print(f"Sorry, no help yet for {cmdname}")
|
|
3198
|
+
|
|
2850
3199
|
async def main(argv):
|
|
2851
3200
|
""" Do the job """
|
|
2852
3201
|
json_output = JSON
|
|
@@ -2865,9 +3214,12 @@ async def main(argv):
|
|
|
2865
3214
|
with open(MCCLI_ADDRESS, encoding="utf-8") as f :
|
|
2866
3215
|
address = f.readline().strip()
|
|
2867
3216
|
|
|
2868
|
-
opts, args = getopt.getopt(argv, "a:d:s:ht:p:b:fjDhvSlT:
|
|
3217
|
+
opts, args = getopt.getopt(argv, "a:d:s:ht:p:b:fjDhvSlT:Pc:")
|
|
2869
3218
|
for opt, arg in opts :
|
|
2870
3219
|
match opt:
|
|
3220
|
+
case "-c" :
|
|
3221
|
+
if arg == "off":
|
|
3222
|
+
process_event_message.color = False
|
|
2871
3223
|
case "-d" : # name specified on cmdline
|
|
2872
3224
|
address = arg
|
|
2873
3225
|
case "-a" : # address specified on cmdline
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|