meshcore-cli 1.2.1__py3-none-any.whl → 1.2.3__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.
@@ -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.3"
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"
@@ -193,6 +197,57 @@ process_event_message.print_snr=False
193
197
  process_event_message.color=True
194
198
  process_event_message.last_node=None
195
199
 
200
+ async def handle_log_rx(event):
201
+ mc = handle_log_rx.mc
202
+ if handle_log_rx.json_log_rx: # json mode ... raw dump
203
+ msg = json.dumps(event.payload)
204
+ if handle_message.above:
205
+ print_above(msg)
206
+ else :
207
+ print(msg)
208
+ return
209
+
210
+ pkt = bytes().fromhex(event.payload["payload"])
211
+
212
+ if handle_log_rx.channel_echoes:
213
+ if pkt[0] == 0x15:
214
+ path_len = pkt[1]
215
+ path = pkt[2:path_len+2].hex()
216
+ chan_hash = pkt[path_len+2:path_len+3].hex()
217
+ cipher_mac = pkt[path_len+3:path_len+5]
218
+ msg = pkt[path_len+5:]
219
+ channel = None
220
+ for c in await get_channels(mc):
221
+ if c["channel_hash"] == chan_hash : # validate against MAC
222
+ h = HMAC.new(bytes.fromhex(c["channel_secret"]), digestmod=SHA256)
223
+ h.update(msg)
224
+ if h.digest()[0:2] == cipher_mac:
225
+ channel = c
226
+ break
227
+
228
+ if channel is None :
229
+ chan_name = chan_hash
230
+ message = msg.hex()
231
+ else:
232
+ chan_name = channel["channel_name"]
233
+ aes_key = bytes.fromhex(channel["channel_secret"])
234
+ cipher = AES.new(aes_key, AES.MODE_ECB)
235
+ message = cipher.decrypt(msg)[5:].decode("utf-8").strip("\x00")
236
+
237
+ width = os.get_terminal_size().columns
238
+ cars = width - 13 - 2 * path_len - len(chan_name) - 1
239
+ dispmsg = message[0:cars]
240
+ 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}"
241
+ if handle_message.above:
242
+ print_above(txt)
243
+ else:
244
+ print(txt)
245
+ return
246
+
247
+ handle_log_rx.json_log_rx = False
248
+ handle_log_rx.channel_echoes = False
249
+ handle_log_rx.mc = None
250
+
196
251
  async def handle_advert(event):
197
252
  if not handle_advert.print_adverts:
198
253
  return
@@ -320,7 +375,7 @@ class MyNestedCompleter(NestedCompleter):
320
375
  opts = self.options.keys()
321
376
  completer = WordCompleter(
322
377
  opts, ignore_case=self.ignore_case,
323
- pattern=re.compile(r"([a-zA-Z0-9_\\/]+|[^a-zA-Z0-9_\s]+)"))
378
+ pattern=re.compile(r"([a-zA-Z0-9_\\/\#]+|[^a-zA-Z0-9_\s\#]+)"))
324
379
  yield from completer.get_completions(document, complete_event)
325
380
  else: # normal behavior for remainder
326
381
  yield from super().get_completions(document, complete_event)
@@ -417,6 +472,8 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
417
472
  "color" : {"on":None, "off":None},
418
473
  "print_name" : {"on":None, "off":None},
419
474
  "print_adverts" : {"on":None, "off":None},
475
+ "json_log_rx" : {"on":None, "off":None},
476
+ "channel_echoes" : {"on":None, "off":None},
420
477
  "print_new_contacts" : {"on": None, "off":None},
421
478
  "print_path_updates" : {"on":None,"off":None},
422
479
  "classic_prompt" : {"on" : None, "off":None},
@@ -444,6 +501,8 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
444
501
  "color":None,
445
502
  "print_name":None,
446
503
  "print_adverts":None,
504
+ "json_log_rx":None,
505
+ "channel_echoes":None,
447
506
  "print_path_updates":None,
448
507
  "print_new_contacts":None,
449
508
  "classic_prompt":None,
@@ -604,6 +663,14 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
604
663
 
605
664
  completion_list.update(slash_contacts_completion_list)
606
665
 
666
+ slash_chan_completion_list = {}
667
+ if not channels is None:
668
+ for c in channels :
669
+ if c["channel_name"] != "":
670
+ slash_chan_completion_list["/" + c["channel_name"]] = None
671
+
672
+ completion_list.update(slash_chan_completion_list)
673
+
607
674
  completion_list.update({
608
675
  "script" : None,
609
676
  "quit" : None
@@ -675,7 +742,7 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
675
742
  if classic :
676
743
  prompt = prompt + " > "
677
744
  else :
678
- prompt = prompt + "🭨"
745
+ prompt = prompt + f"{ANSI_NORMAL}🭬{ANSI_INVERT}"
679
746
 
680
747
  if not contact is None :
681
748
  if not last_ack:
@@ -696,7 +763,7 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
696
763
  prompt = prompt + f"{ANSI_INVERT}"
697
764
 
698
765
  if print_name and not classic :
699
- prompt = prompt + "🭬"
766
+ prompt = prompt + f"{ANSI_NORMAL}🭨{ANSI_INVERT}"
700
767
 
701
768
  prompt = prompt + f"{contact['adv_name']}"
702
769
  if classic :
@@ -728,14 +795,31 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
728
795
 
729
796
  # raw meshcli command as on command line
730
797
  elif line.startswith("$") :
731
- args = shlex.split(line[1:])
732
- await process_cmds(mc, args)
798
+ try :
799
+ args = shlex.split(line[1:])
800
+ await process_cmds(mc, args)
801
+ except ValueError:
802
+ logger.error("Error parsing line {line[1:]}")
733
803
 
734
804
  elif line.startswith("/") :
735
805
  path = line.split(" ", 1)[0]
736
806
  if path.count("/") == 1:
737
- args = shlex.split(line[1:])
738
- await process_cmds(mc, args)
807
+ args = line[1:].split(" ")
808
+ tct = mc.get_contact_by_name(args[0])
809
+ if len(args)>1 and not tct is None: # a contact, send a message
810
+ if tct["type"] == 1 or tct["type"] == 3: # client or room
811
+ last_ack = await msg_ack(mc, tct, line.split(" ", 1)[1])
812
+ else:
813
+ print("Can only send msg to chan, client or room")
814
+ else :
815
+ ch = await get_channel_by_name(mc, args[0])
816
+ if len(args)>1 and not ch is None: # a channel, send message
817
+ await send_chan_msg(mc, ch["channel_idx"], line.split(" ", 1)[1])
818
+ else :
819
+ try :
820
+ await process_cmds(mc, shlex.split(line[1:]))
821
+ except ValueError:
822
+ logger.error(f"Error processing line{line[1:]}")
739
823
  else:
740
824
  cmdline = line[1:].split("/",1)[1]
741
825
  contact_name = path[1:].split("/",1)[0]
@@ -744,10 +828,11 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
744
828
  print(f"{contact_name} is not a contact")
745
829
  else:
746
830
  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])
831
+ if cmdline != "":
832
+ if tct["type"] == 1:
833
+ last_ack = await msg_ack(mc, tct, cmdline)
834
+ else :
835
+ await process_cmds(mc, ["cmd", tct["adv_name"], cmdline])
751
836
 
752
837
  elif line.startswith("to ") : # dest
753
838
  dest = line[3:]
@@ -805,7 +890,7 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
805
890
  if ln is None :
806
891
  print("No received msg yet !")
807
892
  elif ln["type"] == 0 :
808
- await process_cmds(mc, ["chan", str(contact["chan_nb"]), line] )
893
+ await send_chan_msg(mc, ln["chan_nb"], line[1:])
809
894
  else :
810
895
  last_ack = await msg_ack(mc, ln, line[1:])
811
896
  if last_ack == False :
@@ -813,8 +898,11 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
813
898
 
814
899
  # commands are passed through if at root
815
900
  elif contact is None or line.startswith(".") :
816
- args = shlex.split(line)
817
- await process_cmds(mc, args)
901
+ try:
902
+ args = shlex.split(line)
903
+ await process_cmds(mc, args)
904
+ except ValueError:
905
+ logger.error(f"Error processing {line}")
818
906
 
819
907
  elif await process_contact_chat_line(mc, contact, line):
820
908
  pass
@@ -837,7 +925,7 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
837
925
  last_ack = await msg_ack(mc, contact, line)
838
926
 
839
927
  elif contact["type"] == 0 : # channel, send msg to channel
840
- await process_cmds(mc, ["chan", str(contact["chan_nb"]), line] )
928
+ await send_chan_msg(mc, contact["chan_nb"], line)
841
929
 
842
930
  elif contact["type"] == 1 : # chat, send to recipient and wait ack
843
931
  last_ack = await msg_ack(mc, contact, line)
@@ -970,7 +1058,18 @@ async def process_contact_chat_line(mc, contact, line):
970
1058
  password_file = ""
971
1059
  password = ""
972
1060
  if os.path.isdir(MCCLI_CONFIG_DIR) :
1061
+ # if a password file exists with node name open it and destroy it
973
1062
  password_file = MCCLI_CONFIG_DIR + contact['adv_name'] + ".pass"
1063
+ if os.path.exists(password_file) :
1064
+ with open(password_file, "r", encoding="utf-8") as f :
1065
+ password=f.readline().strip()
1066
+ os.remove(password_file)
1067
+ password_file = MCCLI_CONFIG_DIR + contact["public_key"] + ".pass"
1068
+ with open(password_file, "w", encoding="utf-8") as f :
1069
+ f.write(password)
1070
+
1071
+ # this is the new correct password file, using pubkey
1072
+ password_file = MCCLI_CONFIG_DIR + contact["public_key"] + ".pass"
974
1073
  if os.path.exists(password_file) :
975
1074
  with open(password_file, "r", encoding="utf-8") as f :
976
1075
  password=f.readline().strip()
@@ -993,6 +1092,9 @@ async def process_contact_chat_line(mc, contact, line):
993
1092
 
994
1093
  if line.startswith("forget_password") or line.startswith("fp"):
995
1094
  password_file = MCCLI_CONFIG_DIR + contact['adv_name'] + ".pass"
1095
+ if os.path.exists(password_file):
1096
+ os.remove(password_file)
1097
+ password_file = MCCLI_CONFIG_DIR + contact['public_key'] + ".pass"
996
1098
  if os.path.exists(password_file):
997
1099
  os.remove(password_file)
998
1100
  try:
@@ -1112,6 +1214,7 @@ async def set_channel (mc, chan, name, key=None):
1112
1214
  return None
1113
1215
 
1114
1216
  info = res.payload
1217
+ info["channel_hash"] = sha256(info["channel_secret"]).digest()[0:1].hex()
1115
1218
  info["channel_secret"] = info["channel_secret"].hex()
1116
1219
 
1117
1220
  if hasattr(mc,'channels') :
@@ -1200,12 +1303,14 @@ async def get_channels (mc, anim=False) :
1200
1303
  if res.type == EventType.ERROR:
1201
1304
  break
1202
1305
  info = res.payload
1306
+ info["channel_hash"] = sha256(info["channel_secret"]).digest()[0:1].hex()
1203
1307
  info["channel_secret"] = info["channel_secret"].hex()
1204
1308
  mc.channels.append(info)
1205
1309
  ch = ch + 1
1206
1310
  if anim:
1207
1311
  print(".", end="", flush=True)
1208
- print (" Done")
1312
+ if anim:
1313
+ print (" Done")
1209
1314
  return mc.channels
1210
1315
 
1211
1316
  async def print_trace_to (mc, contact):
@@ -1411,6 +1516,14 @@ async def next_cmd(mc, cmds, json_output=False):
1411
1516
  process_event_message.print_snr = (cmds[2] == "on")
1412
1517
  if json_output :
1413
1518
  print(json.dumps({"cmd" : cmds[1], "param" : cmds[2]}))
1519
+ case "json_log_rx" :
1520
+ handle_log_rx.json_log_rx = (cmds[2] == "on")
1521
+ if json_output :
1522
+ print(json.dumps({"cmd" : cmds[1], "param" : cmds[2]}))
1523
+ case "channel_echoes" :
1524
+ handle_log_rx.channel_echoes = (cmds[2] == "on")
1525
+ if json_output :
1526
+ print(json.dumps({"cmd" : cmds[1], "param" : cmds[2]}))
1414
1527
  case "print_adverts" :
1415
1528
  handle_advert.print_adverts = (cmds[2] == "on")
1416
1529
  if json_output :
@@ -1641,6 +1754,16 @@ async def next_cmd(mc, cmds, json_output=False):
1641
1754
  print(json.dumps({"color" : process_event_message.color}))
1642
1755
  else:
1643
1756
  print(f"{'on' if process_event_message.color else 'off'}")
1757
+ case "json_log_rx":
1758
+ if json_output :
1759
+ print(json.dumps({"json_log_rx" : handle_log_rx.json_log_rx}))
1760
+ else:
1761
+ print(f"{'on' if handle_log_rx.json_log_rx else 'off'}")
1762
+ case "channel_echoes":
1763
+ if json_output :
1764
+ print(json.dumps({"channel_echoes" : handle_log_rx.channel_echoes}))
1765
+ else:
1766
+ print(f"{'on' if handle_log_rx.channel_echoes else 'off'}")
1644
1767
  case "print_adverts":
1645
1768
  if json_output :
1646
1769
  print(json.dumps({"print_adverts" : handle_advert.print_adverts}))
@@ -2511,7 +2634,7 @@ async def next_cmd(mc, cmds, json_output=False):
2511
2634
  if json_output:
2512
2635
  await ps.prompt_async()
2513
2636
  else:
2514
- await ps.prompt_async("Press Enter to continue ...")
2637
+ await ps.prompt_async("Press Enter to continue ...\n")
2515
2638
  except (EOFError, KeyboardInterrupt, asyncio.CancelledError):
2516
2639
  pass
2517
2640
 
@@ -2604,8 +2727,11 @@ async def process_script(mc, file, json_output=False):
2604
2727
  line = line.strip()
2605
2728
  if not (line == "" or line[0] == "#"):
2606
2729
  logger.debug(f"processing {line}")
2607
- cmds = shlex.split(line)
2608
- await process_cmds(mc, cmds, json_output)
2730
+ try :
2731
+ cmds = shlex.split(line)
2732
+ await process_cmds(mc, cmds, json_output)
2733
+ except ValueError:
2734
+ logger.error(f"Error processing {line}")
2609
2735
 
2610
2736
  def version():
2611
2737
  print (f"meshcore-cli: command line interface to MeshCore companion radios {VERSION}")
@@ -2881,10 +3007,12 @@ async def main(argv):
2881
3007
  handle_message.mc = mc # connect meshcore to handle_message
2882
3008
  handle_advert.mc = mc
2883
3009
  handle_path_update.mc = mc
3010
+ handle_log_rx.mc = mc
2884
3011
 
2885
3012
  mc.subscribe(EventType.ADVERTISEMENT, handle_advert)
2886
3013
  mc.subscribe(EventType.PATH_UPDATE, handle_path_update)
2887
3014
  mc.subscribe(EventType.NEW_CONTACT, handle_new_contact)
3015
+ mc.subscribe(EventType.RX_LOG_DATA, handle_log_rx)
2888
3016
 
2889
3017
  mc.auto_update_contacts = True
2890
3018
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcore-cli
3
- Version: 1.2.1
3
+ Version: 1.2.3
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
@@ -12,6 +12,7 @@ Classifier: Programming Language :: Python :: 3
12
12
  Requires-Python: >=3.10
13
13
  Requires-Dist: meshcore>=2.1.19
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=4vJusi30D_UBxw9lq4mTO5NqLp8y3zpTb09rCcF7064,126909
4
+ meshcore_cli-1.2.3.dist-info/METADATA,sha256=Z727GJ6CD7OzRfEUGose_dAfACX6Oo2IbM3kzBWihtc,11657
5
+ meshcore_cli-1.2.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
6
+ meshcore_cli-1.2.3.dist-info/entry_points.txt,sha256=77V29Pyth11GteDk7tneBN3MMk8JI7bTlS-BGSmxCmI,103
7
+ meshcore_cli-1.2.3.dist-info/licenses/LICENSE,sha256=F9s987VtS0AKxW7LdB2EkLMkrdeERI7ICdLJR60A9M4,1066
8
+ meshcore_cli-1.2.3.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,,