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.
@@ -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.1"
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"] = data["pubkey_prefix"]
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_\\/]+|[^a-zA-Z0-9_\s]+)"))
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
- args = shlex.split(line[1:])
732
- 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")
733
874
 
734
875
  elif line.startswith("/") :
735
876
  path = line.split(" ", 1)[0]
736
877
  if path.count("/") == 1:
737
- args = shlex.split(line[1:])
738
- await process_cmds(mc, args)
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 tct["type"] == 1:
748
- last_ack = await msg_ack(mc, tct, cmdline)
749
- else :
750
- await process_cmds(mc, ["cmd", tct["adv_name"], 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])
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 process_cmds(mc, ["chan", str(contact["chan_nb"]), line] )
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
- args = shlex.split(line)
817
- await process_cmds(mc, args)
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 process_cmds(mc, ["chan", str(contact["chan_nb"]), line] )
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 line.startswith("cmd ") or\
960
- line.startswith("cp ") or line.startswith("change_path ") or\
961
- line.startswith("cf ") or line.startswith("change_flags ") or\
962
- line.startswith("req_binary ") or\
963
- line.startswith("login ") :
1164
+ if " " in line:
964
1165
  cmds = line.split(" ", 1)
965
- args = [cmds[0], contact['adv_name'], cmds[1]]
966
- await process_cmds(mc, args)
967
- return True
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
- print (" Done")
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}🭬",end="")
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}, will exit ...")
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, returning")
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
- cmds = shlex.split(line)
2608
- 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}")
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(""" General commands
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 <key> : manually add pending contact from key
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.1
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.19
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,,