meshcore-cli 1.3.0__tar.gz → 1.3.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcore-cli
3
- Version: 1.3.0
3
+ Version: 1.3.1
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "meshcore-cli"
7
- version = "1.3.0"
7
+ version = "1.3.1"
8
8
  authors = [
9
9
  { name="Florent de Lamotte", email="florent@frizoncorrea.fr" },
10
10
  ]
@@ -32,7 +32,7 @@ import re
32
32
  from meshcore import MeshCore, EventType, logger
33
33
 
34
34
  # Version
35
- VERSION = "v1.3.0"
35
+ VERSION = "v1.3.1"
36
36
 
37
37
  # default ble address is stored in a config file
38
38
  MCCLI_CONFIG_DIR = str(Path.home()) + "/.config/meshcore/"
@@ -40,6 +40,10 @@ MCCLI_ADDRESS = MCCLI_CONFIG_DIR + "default_address"
40
40
  MCCLI_HISTORY_FILE = MCCLI_CONFIG_DIR + "history"
41
41
  MCCLI_INIT_SCRIPT = MCCLI_CONFIG_DIR + "init"
42
42
 
43
+ PAYLOAD_TYPENAMES = ["REQ", "RESPONSE", "TEXT_MSG", "ACK", "ADVERT", "GRP_TXT", "GRP_DATA", "ANON_REQ", "PATH", "TRACE", "MULTIPART", "CONTROL"]
44
+ ROUTE_TYPENAMES = ["TC_FLOOD", "FLOOD", "DIRECT", "TC_DIRECT"]
45
+ CONTACT_TYPENAMES = ["NONE","CLI","REP","ROOM","SENS"]
46
+
43
47
  # Fallback address if config file not found
44
48
  # if None or "" then a scan is performed
45
49
  ADDRESS = ""
@@ -203,9 +207,6 @@ process_event_message.print_snr=False
203
207
  process_event_message.color=True
204
208
  process_event_message.last_node=None
205
209
 
206
- PAYLOAD_TYPENAMES = ["REQ", "RESPONSE", "TEXT_MSG", "ACK", "ADVERT", "GRP_TXT", "GRP_DATA", "ANON_REQ", "PATH", "TRACE", "MULTIPART", "CONTROL"]
207
- ROUTE_TYPENAMES = ["TC_FLOOD", "FLOOD", "DIRECT", "TC_DIRECT"]
208
-
209
210
  async def handle_log_rx(event):
210
211
  mc = handle_log_rx.mc
211
212
 
@@ -431,7 +432,7 @@ class MyNestedCompleter(NestedCompleter):
431
432
  opts = self.options.keys()
432
433
  completer = WordCompleter(
433
434
  opts, ignore_case=self.ignore_case,
434
- pattern=re.compile(r"([a-zA-Z0-9_\\/\#]+|[^a-zA-Z0-9_\s\#]+)"))
435
+ pattern=re.compile(r"([a-zA-Z0-9_\\/\#\?]+|[^a-zA-Z0-9_\s\#\?]+)"))
435
436
  yield from completer.get_completions(document, complete_event)
436
437
  else: # normal behavior for remainder
437
438
  yield from super().get_completions(document, complete_event)
@@ -582,11 +583,21 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
582
583
  "flood_after":None,
583
584
  "custom":None,
584
585
  },
586
+ "?get":None,
587
+ "?set":None,
588
+ "?scope":None,
589
+ "?contact_info":None,
590
+ "?apply_to":None,
591
+ "?at":None,
592
+ "?node_discover":None,
593
+ "?nd":None,
585
594
  }
586
595
 
587
596
  contact_completion_list = {
588
597
  "contact_info": None,
589
598
  "contact_name": None,
599
+ "contact_key": None,
600
+ "contact_type": None,
590
601
  "contact_lastmod": None,
591
602
  "export_contact" : None,
592
603
  "share_contact" : None,
@@ -749,7 +760,7 @@ make_completion_dict.custom_vars = {}
749
760
  async def interactive_loop(mc, to=None) :
750
761
  print("""Interactive mode, most commands from terminal chat should work.
751
762
  Use \"to\" to select recipient, use Tab to complete name ...
752
- Line starting with \"$\" or \".\" will issue a meshcli command.
763
+ Some cmds have an help accessible with ?<cmd>. Do ?[Tab] to get a list.
753
764
  \"quit\", \"q\", CTRL+D will end interactive mode""")
754
765
 
755
766
  contact = to
@@ -1102,6 +1113,26 @@ async def process_contact_chat_line(mc, contact, line):
1102
1113
  await process_cmds(mc, args)
1103
1114
  return True
1104
1115
 
1116
+ if line.startswith("contact_key") or line.startswith("ck"):
1117
+ print(contact['public_key'],end="")
1118
+ if " " in line:
1119
+ print(" ", end="", flush=True)
1120
+ secline = line.split(" ", 1)[1]
1121
+ await process_contact_chat_line(mc, contact, secline)
1122
+ else:
1123
+ print("")
1124
+ return True
1125
+
1126
+ if line.startswith("contact_type") or line.startswith("ct"):
1127
+ print(f"{CONTACT_TYPENAMES[contact['type']]:4}",end="")
1128
+ if " " in line:
1129
+ print(" ", end="", flush=True)
1130
+ secline = line.split(" ", 1)[1]
1131
+ await process_contact_chat_line(mc, contact, secline)
1132
+ else:
1133
+ print("")
1134
+ return True
1135
+
1105
1136
  if line.startswith("contact_name") or line.startswith("cn"):
1106
1137
  print(contact['adv_name'],end="")
1107
1138
  if " " in line:
@@ -1112,6 +1143,21 @@ async def process_contact_chat_line(mc, contact, line):
1112
1143
  print("")
1113
1144
  return True
1114
1145
 
1146
+ if line.startswith("path") :
1147
+ if contact['out_path_len'] == -1:
1148
+ print("Flood", end="")
1149
+ elif contact['out_path_len'] == 0:
1150
+ print("0 hop", end="")
1151
+ else:
1152
+ print(contact['out_path'],end="")
1153
+ if " " in line:
1154
+ print(" ", end="", flush=True)
1155
+ secline = line.split(" ", 1)[1]
1156
+ await process_contact_chat_line(mc, contact, secline)
1157
+ else:
1158
+ print("")
1159
+ return True
1160
+
1115
1161
  if line.startswith("sleep") or line.startswith("s"):
1116
1162
  try:
1117
1163
  sleeptime = int(line.split(" ",2)[1])
@@ -1143,20 +1189,24 @@ async def process_contact_chat_line(mc, contact, line):
1143
1189
  return True
1144
1190
 
1145
1191
  # commands that take contact as second arg will be sent to recipient
1146
- if line == "sc" or line == "share_contact" or\
1147
- line == "ec" or line == "export_contact" or\
1148
- line == "uc" or line == "upload_contact" or\
1149
- line == "rp" or line == "reset_path" or\
1150
- line == "dp" or line == "disc_path" or\
1151
- line == "contact_info" or line == "ci" or\
1152
- line == "req_status" or line == "rs" or\
1153
- line == "req_neighbours" or line == "rn" or\
1154
- line == "req_telemetry" or line == "rt" or\
1155
- line == "req_acl" or\
1156
- line == "path" or\
1157
- line == "logout" :
1158
- args = [line, contact['adv_name']]
1192
+ # and can be chained ...
1193
+ if line.startswith("sc") or line.startswith("share_contact") or\
1194
+ line.startswith("ec") or line.startswith("export_contact") or\
1195
+ line.startswith("uc") or line.startswith("upload_contact") or\
1196
+ line.startswith("rp") or line.startswith("reset_path") or\
1197
+ line.startswith("dp") or line.startswith("disc_path") or\
1198
+ line.startswith("contact_info") or line.startswith("ci") or\
1199
+ line.startswith("req_status") or line.startswith("rs") or\
1200
+ line.startswith("req_neighbours") or line.startswith("rn") or\
1201
+ line.startswith("req_telemetry") or line.startswith("rt") or\
1202
+ line.startswith("req_acl") or\
1203
+ line.startswith("path") or\
1204
+ line.startswith("logout") :
1205
+ args = [line.split()[0], contact['adv_name']]
1159
1206
  await process_cmds(mc, args)
1207
+ if " " in line:
1208
+ secline = line.split(" ", 1)[1]
1209
+ await process_contact_chat_line(mc, contact, secline)
1160
1210
  return True
1161
1211
 
1162
1212
  # special case for rp that can be chained from cmdline
@@ -1306,12 +1356,13 @@ async def process_contact_chat_line(mc, contact, line):
1306
1356
 
1307
1357
  return False
1308
1358
 
1309
- async def apply_command_to_contacts(mc, contact_filter, line):
1359
+ async def apply_command_to_contacts(mc, contact_filter, line, json_output=False):
1310
1360
  upd_before = None
1311
1361
  upd_after = None
1312
1362
  contact_type = None
1313
1363
  min_hops = None
1314
1364
  max_hops = None
1365
+ count = 0
1315
1366
 
1316
1367
  await mc.ensure_contacts()
1317
1368
 
@@ -1366,6 +1417,9 @@ async def apply_command_to_contacts(mc, contact_filter, line):
1366
1417
  (upd_after is None or contact["lastmod"] > upd_after) and\
1367
1418
  (min_hops is None or contact["out_path_len"] >= min_hops) and\
1368
1419
  (max_hops is None or contact["out_path_len"] <= max_hops):
1420
+
1421
+ count = count + 1
1422
+
1369
1423
  if await process_contact_chat_line(mc, contact, line):
1370
1424
  pass
1371
1425
 
@@ -1390,6 +1444,9 @@ async def apply_command_to_contacts(mc, contact_filter, line):
1390
1444
  else:
1391
1445
  logger.error(f"Can't send {line} to {contact['adv_name']}")
1392
1446
 
1447
+ if not json_output:
1448
+ print(f"> {count} matches in contacts")
1449
+
1393
1450
  async def send_cmd (mc, contact, cmd) :
1394
1451
  res = await mc.commands.send_cmd(contact, cmd)
1395
1452
  if not res is None and not res.type == EventType.ERROR:
@@ -1780,7 +1837,7 @@ async def next_cmd(mc, cmds, json_output=False):
1780
1837
 
1781
1838
  case "apply_to"|"at":
1782
1839
  argnum = 2
1783
- await apply_command_to_contacts(mc, cmds[1], cmds[2])
1840
+ await apply_command_to_contacts(mc, cmds[1], cmds[2], json_output=json_output)
1784
1841
 
1785
1842
  case "set":
1786
1843
  argnum = 2
@@ -2532,18 +2589,14 @@ async def next_cmd(mc, cmds, json_output=False):
2532
2589
  await mc.ensure_contacts()
2533
2590
  print(f"Discovered {len(dn)} nodes:")
2534
2591
  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:
2592
+ try :
2593
+ name = f"{n['pubkey'][0:2]} {mc.get_contact_by_key_prefix(n['pubkey'])['adv_name']}"
2594
+ except TypeError:
2537
2595
  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"
2596
+ if n['node_type'] >= len(CONTACT_TYPENAMES):
2597
+ type = f"t:{n['node_type']}"
2598
+ else:
2599
+ type = CONTACT_TYPENAMES[n['node_type']]
2547
2600
 
2548
2601
  print(f" {name:16} {type:>4} SNR: {n['SNR_in']:6,.2f}->{n['SNR']:6,.2f} RSSI: ->{n['RSSI']:4}")
2549
2602
 
@@ -2561,7 +2614,7 @@ async def next_cmd(mc, cmds, json_output=False):
2561
2614
  else :
2562
2615
  print(json.dumps({
2563
2616
  "name": contact["adv_name"],
2564
- "pubkey_pre": contact["public_key"][0:12],
2617
+ "pubkey_pre": contact["public_key"][0:16],
2565
2618
  "lpp": res,
2566
2619
  }, indent = 4))
2567
2620
 
@@ -2695,7 +2748,13 @@ async def next_cmd(mc, cmds, json_output=False):
2695
2748
  print(json.dumps(res, indent=4))
2696
2749
  else :
2697
2750
  for c in res.items():
2698
- print(c[1]["adv_name"])
2751
+ if c[1]['out_path_len'] == -1:
2752
+ path_str = "Flood"
2753
+ elif c[1]['out_path_len'] == 0:
2754
+ path_str = "0 hop"
2755
+ else:
2756
+ path_str = f"{c[1]['out_path']}"
2757
+ print(f"{c[1]['adv_name']:30} {CONTACT_TYPENAMES[c[1]['type']]:4} {c[1]['public_key'][:12]}  {path_str}")
2699
2758
  print(f"> {len(mc.contacts)} contacts in device")
2700
2759
 
2701
2760
  case "reload_contacts" | "rc":
@@ -2763,7 +2822,7 @@ async def next_cmd(mc, cmds, json_output=False):
2763
2822
  if (path_len == 0) :
2764
2823
  print("0 hop")
2765
2824
  elif (path_len == -1) :
2766
- print("Path not set")
2825
+ print("Flood")
2767
2826
  else:
2768
2827
  print(path)
2769
2828
 
@@ -2790,6 +2849,8 @@ async def next_cmd(mc, cmds, json_output=False):
2790
2849
  print(f"Unknown contact {cmds[1]}")
2791
2850
  else:
2792
2851
  path = cmds[2].replace(",","") # we'll accept path with ,
2852
+ if path == "0":
2853
+ path = ""
2793
2854
  try:
2794
2855
  res = await mc.commands.change_contact_path(contact, path)
2795
2856
  logger.debug(res)
@@ -3213,7 +3274,7 @@ def get_help_for (cmdname, context="line") :
3213
3274
  - d, direct, similar to h>-1
3214
3275
  - f, flood, similar to h<0 or h=-1
3215
3276
 
3216
- Note: Some commands like contact_name (aka cn), reset_path (aka rp), forget_password (aka fp) can be chained. There is also a sleep command taking an optional event. The sleep will be issued after the command, it helps limiting rate through repeaters ...
3277
+ Note: Some commands like contact_name (aka cn), contact_key (aka ck), contact_type (aka ct), reset_path (aka rp), forget_password (aka fp) can be chained. There is also a sleep command taking an optional event. The sleep will be issued after the command, it helps limiting rate through repeaters ...
3217
3278
 
3218
3279
  Examples:
3219
3280
  # removes all clients that have not been updated in last 2 days
@@ -3252,7 +3313,8 @@ def get_help_for (cmdname, context="line") :
3252
3313
  print_new_contacts : display new pending contacts when available
3253
3314
  print_path_updates : display path updates as they come
3254
3315
  custom : all custom variables in json format
3255
- each custom var can also be get/set directly""")
3316
+ each custom var can also be get/set directly
3317
+ """)
3256
3318
 
3257
3319
  elif cmdname == "set" :
3258
3320
  print("""Available parameters :
@@ -3285,7 +3347,8 @@ def get_help_for (cmdname, context="line") :
3285
3347
  arrow_head <string> : change arrow head in prompt
3286
3348
  slash_start <string> : idem for slash start
3287
3349
  slash_end <string> : slash end
3288
- invert_slash <on/off> : apply color inversion to slash """)
3350
+ invert_slash <on/off> : apply color inversion to slash
3351
+ """)
3289
3352
 
3290
3353
  elif cmdname == "scope":
3291
3354
  print("""scope <scope> : changes flood scope of the node
@@ -3296,7 +3359,17 @@ Managing Flood Scope in interactive mode
3296
3359
  Flood scope has recently been introduced in meshcore (from v1.10.0). It limits the scope of packets to regions, using transport codes in the frame.
3297
3360
  When entering chat mode, scope will be reset to *, meaning classic flood.
3298
3361
  You can switch scope using the scope command, or postfixing the to command with %<scope>.
3299
- Scope can also be applied to a command using % before the scope name. For instance login%#Morbihan will limit diffusion of the login command (which is usually sent flood to get the path to a repeater) to the #Morbihan region.""")
3362
+ Scope can also be applied to a command using % before the scope name. For instance login%#Morbihan will limit diffusion of the login command (which is usually sent flood to get the path to a repeater) to the #Morbihan region.
3363
+ """)
3364
+
3365
+ elif cmdname == "contact_info":
3366
+ print("""contact_info <ct> : displays contact info
3367
+
3368
+ in interactive mode, there are some lighter commands that can be chained to give more compact information
3369
+ - contact_name (cn)
3370
+ - contact_key (ck)
3371
+ - contact_type (ct)
3372
+ """)
3300
3373
 
3301
3374
  else:
3302
3375
  print(f"Sorry, no help yet for {cmdname}")
File without changes
File without changes
File without changes
File without changes
File without changes