meshcore-cli 1.3.9__py3-none-any.whl → 1.3.12__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.
@@ -32,7 +32,7 @@ import re
32
32
  from meshcore import MeshCore, EventType, logger
33
33
 
34
34
  # Version
35
- VERSION = "v1.3.9"
35
+ VERSION = "v1.3.12"
36
36
 
37
37
  # default ble address is stored in a config file
38
38
  MCCLI_CONFIG_DIR = str(Path.home()) + "/.config/meshcore/"
@@ -72,6 +72,8 @@ ANSI_LIGHT_GREEN = "\033[0;92m"
72
72
  ANSI_LIGHT_YELLOW = "\033[0;93m"
73
73
  ANSI_LIGHT_GRAY="\033[0;38;5;247m"
74
74
  ANSI_BGRAY="\033[1;38;5;247m"
75
+ ANSI_GRAY_BACK="\033[48;5;247m"
76
+ ANSI_RESET_BACK="\033[49m"
75
77
  ANSI_ORANGE="\033[0;38;5;214m"
76
78
  ANSI_BORANGE="\033[1;38;5;214m"
77
79
  #ANSI_YELLOW="\033[0;38;5;226m"
@@ -82,8 +84,9 @@ ANSI_BYELLOW = "\033[1;33m"
82
84
  #Unicode chars
83
85
  # some possible symbols for prompts 🭬🬛🬗🭬🬛🬃🬗🭬🬛🬃🬗🬏🭀🭋🭨🮋
84
86
  ARROW_HEAD = ""
85
- SLASH_END = ""
86
- SLASH_START = ""
87
+ SLASH_END = f"{ANSI_RESET_BACK}"
88
+ #SLASH_START = ""
89
+ SLASH_START = f"{ANSI_GRAY_BACK}"
87
90
  INVERT_SLASH = False
88
91
 
89
92
  def escape_ansi(line):
@@ -294,7 +297,7 @@ async def handle_log_rx(event):
294
297
  chan_name = channel["channel_name"]
295
298
  aes_key = bytes.fromhex(channel["channel_secret"])
296
299
  cipher = AES.new(aes_key, AES.MODE_ECB)
297
- message = cipher.decrypt(msg)[5:].decode("utf-8").strip("\x00")
300
+ message = cipher.decrypt(msg)[5:].decode("utf-8", "ignore").strip("\x00")
298
301
 
299
302
  if chan_name != "" :
300
303
  width = os.get_terminal_size().columns
@@ -324,7 +327,7 @@ async def handle_log_rx(event):
324
327
  if flags & 0x40 > 0: #has feature2
325
328
  adv_feat2 = pk_buf.read(2).hex()
326
329
  if flags & 0x80 > 0: #has name
327
- adv_name = pk_buf.read().decode("utf-8").strip("\x00")
330
+ adv_name = pk_buf.read().decode("utf-8", "ignore").strip("\x00")
328
331
 
329
332
  if adv_name is None:
330
333
  # try to get the name from the contact
@@ -334,7 +337,7 @@ async def handle_log_rx(event):
334
337
  else:
335
338
  adv_name = ct["adv_name"]
336
339
 
337
- ts_string = ""
340
+ ts_str = ""
338
341
  if process_event_message.timestamp != "" and process_event_message.timestamp != "off":
339
342
  ts = adv_timestamp
340
343
  if process_event_message.timestamp == "on":
@@ -443,10 +446,16 @@ async def log_message(mc, msg):
443
446
  ct = mc.get_contact_by_key_prefix(msg['pubkey_prefix'])
444
447
  if ct is None:
445
448
  msg["name"] = msg["pubkey_prefix"]
449
+ msg["sender"] = msg["pubkey_prefix"]
446
450
  else:
447
451
  msg["name"] = ct["adv_name"]
452
+ msg["sender"] = ct["adv_name"]
448
453
  elif msg["type"] == "CHAN" :
449
- msg["name"] = f"channel {msg['channel_idx']}"
454
+ if hasattr(mc, 'channels') :
455
+ msg["sender"] = mc.channels[msg['channel_idx']]["channel_name"]
456
+ else:
457
+ msg["sender"] = f"channel {msg['channel_idx']}"
458
+ msg["name"] = msg["sender"]
450
459
  msg["timestamp"] = int(time.time())
451
460
 
452
461
  with open(log_message.file, "a") as logfile:
@@ -539,6 +548,7 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
539
548
 
540
549
  completion_list = {
541
550
  "to" : to_list,
551
+ "/to" : to_list,
542
552
  "public" : None,
543
553
  "chan" : None,
544
554
  }
@@ -599,6 +609,7 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
599
609
  "lat" : None,
600
610
  "lon" : None,
601
611
  "coords" : None,
612
+ "private_key": None,
602
613
  "print_snr" : {"on":None, "off": None},
603
614
  "print_timestamp" : {"on":None, "off": None, "%Y:%M":None},
604
615
  "json_msgs" : {"on":None, "off": None},
@@ -630,6 +641,7 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
630
641
  "coords":None,
631
642
  "lat":None,
632
643
  "lon":None,
644
+ "private_key":None,
633
645
  "print_snr":None,
634
646
  "print_timestamp":None,
635
647
  "json_msgs":None,
@@ -859,8 +871,6 @@ Some cmds have an help accessible with ?<cmd>. Do ?[Tab] to get a list.
859
871
 
860
872
  await subscribe_to_msgs(mc, above=True)
861
873
 
862
- handle_new_contact.print_new_contacts = True
863
-
864
874
  try:
865
875
  if os.path.isdir(MCCLI_CONFIG_DIR) :
866
876
  our_history = FileHistory(MCCLI_HISTORY_FILE)
@@ -1000,6 +1010,9 @@ Some cmds have an help accessible with ?<cmd>. Do ?[Tab] to get a list.
1000
1010
  except IndexError:
1001
1011
  print(scope)
1002
1012
 
1013
+ elif line == "quit" or line == "q" or line == "/quit" or line == "/q" :
1014
+ break
1015
+
1003
1016
  elif contact is None and (line.startswith("apply_to ") or line.startswith("at ")) or\
1004
1017
  line.startswith("/apply_to ") or line.startswith("/at ") :
1005
1018
  try:
@@ -1007,52 +1020,8 @@ Some cmds have an help accessible with ?<cmd>. Do ?[Tab] to get a list.
1007
1020
  except IndexError:
1008
1021
  logger.error(f"Error with apply_to command parameters")
1009
1022
 
1010
- elif line.startswith("/") :
1011
- path = line.split(" ", 1)[0]
1012
- if path.count("/") == 1:
1013
- args = line[1:].split(" ")
1014
- dest = args[0]
1015
- dest_scope = None
1016
- if "%" in dest :
1017
- dest_scope = dest.split("%")[-1]
1018
- dest = dest[:-len(dest_scope)-1]
1019
- await set_scope (mc, dest_scope)
1020
- tct = mc.get_contact_by_name(dest)
1021
- if len(args)>1 and not tct is None: # a contact, send a message
1022
- if tct["type"] == 1 or tct["type"] == 3: # client or room
1023
- last_ack = await msg_ack(mc, tct, line.split(" ", 1)[1])
1024
- else:
1025
- print("Can only send msg to chan, client or room")
1026
- else :
1027
- ch = await get_channel_by_name(mc, dest)
1028
- if len(args)>1 and not ch is None: # a channel, send message
1029
- await send_chan_msg(mc, ch["channel_idx"], line.split(" ", 1)[1])
1030
- else :
1031
- try :
1032
- await process_cmds(mc, shlex.split(line[1:]))
1033
- except ValueError:
1034
- logger.error(f"Error processing line{line[1:]}")
1035
- else:
1036
- cmdline = line[1:].split("/",1)[1]
1037
- contact_name = path[1:].split("/",1)[0]
1038
- dest_scope = None
1039
- if "%" in contact_name:
1040
- dest_scope = contact_name.split("%")[-1]
1041
- contact_name = contact_name[:-len(dest_scope)-1]
1042
- await set_scope (mc, dest_scope)
1043
- tct = mc.get_contact_by_name(contact_name)
1044
- if tct is None:
1045
- print(f"{contact_name} is not a contact")
1046
- else:
1047
- if not await process_contact_chat_line(mc, tct, cmdline):
1048
- if cmdline != "":
1049
- if tct["type"] == 1:
1050
- last_ack = await msg_ack(mc, tct, cmdline)
1051
- else :
1052
- await process_cmds(mc, ["cmd", tct["adv_name"], cmdline])
1053
-
1054
- elif line.startswith("to ") : # dest
1055
- dest = line[3:]
1023
+ elif line.startswith("to ") or line.startswith("/to "): # dest
1024
+ dest = line.split(" ", 1)[1]
1056
1025
  if dest.startswith("\"") or dest.startswith("\'") : # if name starts with a quote
1057
1026
  dest = shlex.split(dest)[0] # use shlex.split to get contact name between quotes
1058
1027
  dest_scope = None
@@ -1098,14 +1067,55 @@ Some cmds have an help accessible with ?<cmd>. Do ?[Tab] to get a list.
1098
1067
  if not dest_scope is None:
1099
1068
  scope = await set_scope(mc, dest_scope)
1100
1069
 
1101
- elif line == "to" :
1070
+ elif line == "to" or line == "/to" :
1102
1071
  if contact is None :
1103
1072
  print(mc.self_info['name'])
1104
1073
  else:
1105
1074
  print(contact["adv_name"])
1106
1075
 
1107
- elif line == "quit" or line == "q" :
1108
- break
1076
+ elif line.startswith("/") :
1077
+ path = line.split(" ", 1)[0]
1078
+ if path.count("/") == 1:
1079
+ args = line[1:].split(" ")
1080
+ dest = args[0]
1081
+ dest_scope = None
1082
+ if "%" in dest :
1083
+ dest_scope = dest.split("%")[-1]
1084
+ dest = dest[:-len(dest_scope)-1]
1085
+ await set_scope (mc, dest_scope)
1086
+ tct = mc.get_contact_by_name(dest)
1087
+ if len(args)>1 and not tct is None: # a contact, send a message
1088
+ if tct["type"] == 1 or tct["type"] == 3: # client or room
1089
+ last_ack = await msg_ack(mc, tct, line.split(" ", 1)[1])
1090
+ else:
1091
+ print("Can only send msg to chan, client or room")
1092
+ else :
1093
+ ch = await get_channel_by_name(mc, dest)
1094
+ if len(args)>1 and not ch is None: # a channel, send message
1095
+ await send_chan_msg(mc, ch["channel_idx"], line.split(" ", 1)[1])
1096
+ else :
1097
+ try :
1098
+ await process_cmds(mc, shlex.split(line[1:]))
1099
+ except ValueError:
1100
+ logger.error(f"Error processing line{line[1:]}")
1101
+ else:
1102
+ cmdline = line[1:].split("/",1)[1]
1103
+ contact_name = path[1:].split("/",1)[0]
1104
+ dest_scope = None
1105
+ if "%" in contact_name:
1106
+ dest_scope = contact_name.split("%")[-1]
1107
+ contact_name = contact_name[:-len(dest_scope)-1]
1108
+ await set_scope (mc, dest_scope)
1109
+ tct = mc.get_contact_by_name(contact_name)
1110
+ if tct is None:
1111
+ print(f"{contact_name} is not a contact")
1112
+ else:
1113
+ if not await process_contact_chat_line(mc, tct, cmdline):
1114
+ if cmdline != "":
1115
+ if tct["type"] == 1:
1116
+ last_ack = await msg_ack(mc, tct, cmdline)
1117
+ else :
1118
+ await process_cmds(mc, ["cmd", tct["adv_name"], cmdline])
1109
1119
 
1110
1120
  # commands that take one parameter (don't need quotes)
1111
1121
  elif line.startswith("public ") :
@@ -1537,10 +1547,11 @@ async def send_cmd (mc, contact, cmd) :
1537
1547
  if isinstance(contact, dict):
1538
1548
  sent = res.payload.copy()
1539
1549
  sent["type"] = "SENT_CMD"
1540
- sent["name"] = contact["adv_name"]
1550
+ sent["recipient"] = contact["adv_name"]
1541
1551
  sent["text"] = cmd
1542
1552
  sent["txt_type"] = 1
1543
- sent["name"] = mc.self_info['name']
1553
+ sent["sender"] = mc.self_info['name']
1554
+ sent["name"] = sent["recipient"]
1544
1555
  await log_message(mc, sent)
1545
1556
  return res
1546
1557
 
@@ -1550,9 +1561,16 @@ async def send_chan_msg(mc, nb, msg):
1550
1561
  sent = res.payload.copy()
1551
1562
  sent["type"] = "SENT_CHAN"
1552
1563
  sent["channel_idx"] = nb
1564
+ if hasattr(mc, "channels"):
1565
+ chan_name = mc.channels[nb]["channel_name"]
1566
+ else:
1567
+ chan_name = f"channel {nb}"
1568
+ sent["chan_name"] = chan_name
1569
+ sent["recipient"] = chan_name
1553
1570
  sent["text"] = msg
1554
1571
  sent["txt_type"] = 0
1555
- sent["name"] = mc.self_info['name']
1572
+ sent["sender"] = mc.self_info['name']
1573
+ sent["name"] = chan_name
1556
1574
  await log_message(mc, sent)
1557
1575
  return res
1558
1576
 
@@ -1563,10 +1581,11 @@ async def send_msg (mc, contact, msg) :
1563
1581
  if isinstance(contact, dict):
1564
1582
  sent = res.payload.copy()
1565
1583
  sent["type"] = "SENT_MSG"
1566
- sent["name"] = contact["adv_name"]
1584
+ sent["recipient"] = contact["adv_name"]
1567
1585
  sent["text"] = msg
1568
1586
  sent["txt_type"] = 0
1569
- sent["name"] = mc.self_info['name']
1587
+ sent["sender"] = mc.self_info['name']
1588
+ sent["name"] = sent["recipient"]
1570
1589
  await log_message(mc, sent)
1571
1590
  return res
1572
1591
 
@@ -1583,10 +1602,11 @@ async def msg_ack (mc, contact, msg) :
1583
1602
  if isinstance(contact, dict):
1584
1603
  sent = res.payload.copy()
1585
1604
  sent["type"] = "SENT_MSG"
1586
- sent["name"] = contact["adv_name"]
1605
+ sent["recipient"] = contact["adv_name"]
1587
1606
  sent["text"] = msg
1588
1607
  sent["txt_type"] = 0
1589
- sent["name"] = mc.self_info['name']
1608
+ sent["sender"] = mc.self_info['name']
1609
+ sent["name"] = sent["recipient"]
1590
1610
  await log_message(mc, sent)
1591
1611
  return not res is None
1592
1612
  msg_ack.max_attempts=3
@@ -1604,8 +1624,11 @@ async def set_scope (mc, scope) :
1604
1624
  return scope
1605
1625
 
1606
1626
  res = await mc.commands.set_flood_scope(scope)
1607
- if res is None or res.type == EventType.ERROR:
1608
- if not res is None and res.payload["error_code"] == 1: #unsupported
1627
+ if res is None :
1628
+ return None
1629
+
1630
+ if res.type == EventType.ERROR:
1631
+ if "error_code" in res.payload and res.payload["error_code"] == 1: #unsupported
1609
1632
  set_scope.has_scope = False
1610
1633
  return None
1611
1634
 
@@ -2073,6 +2096,16 @@ async def next_cmd(mc, cmds, json_output=False):
2073
2096
  print(json.dumps(res.payload, indent=4))
2074
2097
  else:
2075
2098
  print("ok")
2099
+ case "private_key":
2100
+ params=bytes.fromhex(cmds[2])
2101
+ res = await mc.commands.import_private_key(params)
2102
+ logger.debug(res)
2103
+ if res.type == EventType.ERROR:
2104
+ print(f"Error: {res}")
2105
+ elif json_output :
2106
+ print(json.dumps(res.payload, indent=4))
2107
+ else:
2108
+ print("ok")
2076
2109
  case "tuning":
2077
2110
  params=cmds[2].commands.split(",")
2078
2111
  res = await mc.commands.set_tuning(
@@ -2285,6 +2318,16 @@ async def next_cmd(mc, cmds, json_output=False):
2285
2318
  print(json.dumps(res.payload, indent=4))
2286
2319
  else:
2287
2320
  print(f"Battery level : {res.payload['level']}")
2321
+ case "private_key":
2322
+ res = await mc.commands.export_private_key()
2323
+ logger.debug(res)
2324
+ if res.type == EventType.ERROR:
2325
+ print(f"Error exporting private key {res}")
2326
+ elif json_output :
2327
+ res.payload["private_key"] = res.payload["private_key"].hex()
2328
+ print(json.dumps(res.payload))
2329
+ else:
2330
+ print(f"Private key: {res.payload['private_key'].hex()}")
2288
2331
  case "fstats" :
2289
2332
  res = await mc.commands.get_bat()
2290
2333
  logger.debug(res)
@@ -2476,7 +2519,9 @@ async def next_cmd(mc, cmds, json_output=False):
2476
2519
  if cmds[1].isnumeric() :
2477
2520
  nb = int(cmds[1])
2478
2521
  else:
2479
- nb = get_channel_by_name(mc, cmds[1])['channel_idx']
2522
+ chan = await get_channel_by_name(mc, cmds[1])
2523
+ print (chan)
2524
+ nb = chan['channel_idx']
2480
2525
  res = await send_chan_msg(mc, nb, cmds[2])
2481
2526
  logger.debug(res)
2482
2527
  if res.type == EventType.ERROR:
@@ -3431,6 +3476,7 @@ def get_help_for (cmdname, context="line") :
3431
3476
  lon : longitude
3432
3477
  radio : radio parameters
3433
3478
  tx : tx power
3479
+ private_key : private key of the node
3434
3480
  print_snr : snr display in messages
3435
3481
  print_adverts : display adverts as they come
3436
3482
  print_new_contacts : display new pending contacts when available
@@ -3449,6 +3495,7 @@ def get_help_for (cmdname, context="line") :
3449
3495
  name <name> : node name
3450
3496
  lat <lat> : latitude
3451
3497
  lon <lon> : longitude
3498
+ private_key : private key
3452
3499
  coords <lat,lon> : coordinates
3453
3500
  multi_ack <on/off> : multi-acks feature
3454
3501
  telemetry_mode_base <mode> : set basic telemetry mode all/selected/off
@@ -3468,7 +3515,7 @@ def get_help_for (cmdname, context="line") :
3468
3515
  json_log_rx <on/off> : logs packets incoming to device as json
3469
3516
  channel_echoes <on/off> : print repeats for channel data
3470
3517
  advert_echoes <on/off> : print repeats for adverts
3471
- echo_unk_channels <on/off> : also dump unk channels (encrypted)
3518
+ echo_unk_chans <on/off> : also dump unk channels (encrypted)
3472
3519
  color <on/off> : color off should remove ANSI codes from output
3473
3520
  meshcore-cli behaviour:
3474
3521
  classic_prompt <on/off> : activates less fancier prompt
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcore-cli
3
- Version: 1.3.9
3
+ Version: 1.3.12
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
@@ -11,7 +11,7 @@ Classifier: Operating System :: OS Independent
11
11
  Classifier: Programming Language :: Python :: 3
12
12
  Requires-Python: >=3.10
13
13
  Requires-Dist: bleak<2.0,>=0.22
14
- Requires-Dist: meshcore>=2.2.2
14
+ Requires-Dist: meshcore>=2.2.3
15
15
  Requires-Dist: prompt-toolkit>=3.0.50
16
16
  Requires-Dist: pycryptodome
17
17
  Requires-Dist: requests>=2.28.0
@@ -21,6 +21,18 @@ Description-Content-Type: text/markdown
21
21
 
22
22
  meshcore-cli : CLI interface to MeschCore companion app over BLE, TCP or Serial
23
23
 
24
+ ## About
25
+
26
+ meshcore-cli is a tool that connects to your companion radio node (meshcore client) over BLE, TCP or Serial and lets you interact with it from a terminal using a command line interface.
27
+
28
+ You can send commands as parameters to the meshcore-cli command (from your shell) either interactively or through a script.
29
+
30
+ There is also an interactive mode (this is the default when no command is passed). In interactive mode you can enter a contact (another client a repeater, a sensor or a room) and interact with it. For clients, interaction consists in sending/receiving messages. For repeaters, rooms or sensors it will directly give you the remote cli (you can still send messages to rooms using double quote prefix or msg command).
31
+
32
+ Note that meshcore-cli only interacts with companion radios (through BLE, Serial or TCP), you can't connect to a repeater using its serial interface.
33
+
34
+ Also, most meshcore companions only have one interface compiled in at a time. So you can't connect via Serial to a node, which has been compiled as a BLE companion.
35
+
24
36
  ## Install
25
37
 
26
38
  Meshcore-cli depends on the [python meshcore](https://github.com/fdlamotte/meshcore_py) package. You can install both via `pip` or `pipx` using the command:
@@ -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=rDkT80QQIAcymcU6Ht26IUgVclL1ezr9vtz-n_J2Z-s,160907
4
+ meshcore_cli-1.3.12.dist-info/METADATA,sha256=sim9NxysAUn_A_P2c_omTeI52--lYtWk8nTQPtF14SI,18194
5
+ meshcore_cli-1.3.12.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
6
+ meshcore_cli-1.3.12.dist-info/entry_points.txt,sha256=77V29Pyth11GteDk7tneBN3MMk8JI7bTlS-BGSmxCmI,103
7
+ meshcore_cli-1.3.12.dist-info/licenses/LICENSE,sha256=F9s987VtS0AKxW7LdB2EkLMkrdeERI7ICdLJR60A9M4,1066
8
+ meshcore_cli-1.3.12.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=jG9EBCeku171u7xV1ilBn3_3FgiZiXixp3cbc9OdXcw,158679
4
- meshcore_cli-1.3.9.dist-info/METADATA,sha256=jm_5fHDTXA_KBCBzWD7B8nDB21FBERLV-iJExW_HwuA,17137
5
- meshcore_cli-1.3.9.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
6
- meshcore_cli-1.3.9.dist-info/entry_points.txt,sha256=77V29Pyth11GteDk7tneBN3MMk8JI7bTlS-BGSmxCmI,103
7
- meshcore_cli-1.3.9.dist-info/licenses/LICENSE,sha256=F9s987VtS0AKxW7LdB2EkLMkrdeERI7ICdLJR60A9M4,1066
8
- meshcore_cli-1.3.9.dist-info/RECORD,,