meshcore-cli 1.3.0__py3-none-any.whl → 1.3.7__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.
@@ -10,7 +10,7 @@ import getopt, json, shlex, re
10
10
  import logging
11
11
  import requests
12
12
  from bleak import BleakScanner, BleakClient
13
- from bleak.exc import BleakError
13
+ from bleak.exc import BleakError, BleakDBusError
14
14
  import serial.tools.list_ports
15
15
  from pathlib import Path
16
16
  import traceback
@@ -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.7"
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
 
@@ -284,7 +285,7 @@ async def handle_log_rx(event):
284
285
  if chan_name != "" :
285
286
  width = os.get_terminal_size().columns
286
287
  cars = width - 13 - 2 * path_len - len(chan_name) - 1
287
- dispmsg = message[0:cars]
288
+ dispmsg = message.replace("\n","")[0:cars]
288
289
  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}"
289
290
  if handle_message.above:
290
291
  print_above(txt)
@@ -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,24 @@ 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,
594
+ "?pending_contacts":None,
595
+ "?add_pending":None,
596
+ "?flush_pending":None,
585
597
  }
586
598
 
587
599
  contact_completion_list = {
588
600
  "contact_info": None,
589
601
  "contact_name": None,
602
+ "contact_key": None,
603
+ "contact_type": None,
590
604
  "contact_lastmod": None,
591
605
  "export_contact" : None,
592
606
  "share_contact" : None,
@@ -749,9 +763,10 @@ make_completion_dict.custom_vars = {}
749
763
  async def interactive_loop(mc, to=None) :
750
764
  print("""Interactive mode, most commands from terminal chat should work.
751
765
  Use \"to\" to select recipient, use Tab to complete name ...
752
- Line starting with \"$\" or \".\" will issue a meshcli command.
766
+ Some cmds have an help accessible with ?<cmd>. Do ?[Tab] to get a list.
753
767
  \"quit\", \"q\", CTRL+D will end interactive mode""")
754
768
 
769
+
755
770
  contact = to
756
771
  prev_contact = None
757
772
 
@@ -760,16 +775,16 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
760
775
 
761
776
  await get_contacts(mc, anim=True)
762
777
  await get_channels(mc, anim=True)
778
+
779
+ # Call sync_msg before going further so there is no issue when scrolling
780
+ # long list of msgs
781
+ await next_cmd(mc, ["sync_msgs"])
782
+
763
783
  await subscribe_to_msgs(mc, above=True)
764
784
 
765
785
  handle_new_contact.print_new_contacts = True
766
786
 
767
787
  try:
768
- while True: # purge msgs
769
- res = await mc.commands.get_msg()
770
- if res.type == EventType.NO_MORE_MSGS:
771
- break
772
-
773
788
  if os.path.isdir(MCCLI_CONFIG_DIR) :
774
789
  our_history = FileHistory(MCCLI_HISTORY_FILE)
775
790
  else:
@@ -1102,6 +1117,26 @@ async def process_contact_chat_line(mc, contact, line):
1102
1117
  await process_cmds(mc, args)
1103
1118
  return True
1104
1119
 
1120
+ if line.startswith("contact_key") or line.startswith("ck"):
1121
+ print(contact['public_key'],end="")
1122
+ if " " in line:
1123
+ print(" ", end="", flush=True)
1124
+ secline = line.split(" ", 1)[1]
1125
+ await process_contact_chat_line(mc, contact, secline)
1126
+ else:
1127
+ print("")
1128
+ return True
1129
+
1130
+ if line.startswith("contact_type") or line.startswith("ct"):
1131
+ print(f"{CONTACT_TYPENAMES[contact['type']]:4}",end="")
1132
+ if " " in line:
1133
+ print(" ", end="", flush=True)
1134
+ secline = line.split(" ", 1)[1]
1135
+ await process_contact_chat_line(mc, contact, secline)
1136
+ else:
1137
+ print("")
1138
+ return True
1139
+
1105
1140
  if line.startswith("contact_name") or line.startswith("cn"):
1106
1141
  print(contact['adv_name'],end="")
1107
1142
  if " " in line:
@@ -1112,7 +1147,22 @@ async def process_contact_chat_line(mc, contact, line):
1112
1147
  print("")
1113
1148
  return True
1114
1149
 
1115
- if line.startswith("sleep") or line.startswith("s"):
1150
+ if line.startswith("path") :
1151
+ if contact['out_path_len'] == -1:
1152
+ print("Flood", end="")
1153
+ elif contact['out_path_len'] == 0:
1154
+ print("0 hop", end="")
1155
+ else:
1156
+ print(contact['out_path'],end="")
1157
+ if " " in line:
1158
+ print(" ", end="", flush=True)
1159
+ secline = line.split(" ", 1)[1]
1160
+ await process_contact_chat_line(mc, contact, secline)
1161
+ else:
1162
+ print("")
1163
+ return True
1164
+
1165
+ if line.startswith("sleep ") or line.startswith("s "):
1116
1166
  try:
1117
1167
  sleeptime = int(line.split(" ",2)[1])
1118
1168
  cmd_pos = 2
@@ -1143,20 +1193,24 @@ async def process_contact_chat_line(mc, contact, line):
1143
1193
  return True
1144
1194
 
1145
1195
  # 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']]
1196
+ # and can be chained ...
1197
+ if line.startswith("sc") or line.startswith("share_contact") or\
1198
+ line.startswith("ec") or line.startswith("export_contact") or\
1199
+ line.startswith("uc") or line.startswith("upload_contact") or\
1200
+ line.startswith("rp") or line.startswith("reset_path") or\
1201
+ line.startswith("dp") or line.startswith("disc_path") or\
1202
+ line.startswith("contact_info") or line.startswith("ci") or\
1203
+ line.startswith("req_status") or line.startswith("rs") or\
1204
+ line.startswith("req_neighbours") or line.startswith("rn") or\
1205
+ line.startswith("req_telemetry") or line.startswith("rt") or\
1206
+ line.startswith("req_acl") or\
1207
+ line.startswith("path") or\
1208
+ line.startswith("logout") :
1209
+ args = [line.split()[0], contact['adv_name']]
1159
1210
  await process_cmds(mc, args)
1211
+ if " " in line:
1212
+ secline = line.split(" ", 1)[1]
1213
+ await process_contact_chat_line(mc, contact, secline)
1160
1214
  return True
1161
1215
 
1162
1216
  # special case for rp that can be chained from cmdline
@@ -1169,6 +1223,8 @@ async def process_contact_chat_line(mc, contact, line):
1169
1223
 
1170
1224
  if line.startswith("set timeout "):
1171
1225
  cmds=line.split(" ")
1226
+ #args = ["contact_timeout", contact['adv_name'], cmds[2]]
1227
+ #await process_cmds(mc, args)
1172
1228
  contact["timeout"] = float(cmds[2])
1173
1229
  return True
1174
1230
 
@@ -1306,12 +1362,13 @@ async def process_contact_chat_line(mc, contact, line):
1306
1362
 
1307
1363
  return False
1308
1364
 
1309
- async def apply_command_to_contacts(mc, contact_filter, line):
1365
+ async def apply_command_to_contacts(mc, contact_filter, line, json_output=False):
1310
1366
  upd_before = None
1311
1367
  upd_after = None
1312
1368
  contact_type = None
1313
1369
  min_hops = None
1314
1370
  max_hops = None
1371
+ count = 0
1315
1372
 
1316
1373
  await mc.ensure_contacts()
1317
1374
 
@@ -1366,6 +1423,9 @@ async def apply_command_to_contacts(mc, contact_filter, line):
1366
1423
  (upd_after is None or contact["lastmod"] > upd_after) and\
1367
1424
  (min_hops is None or contact["out_path_len"] >= min_hops) and\
1368
1425
  (max_hops is None or contact["out_path_len"] <= max_hops):
1426
+
1427
+ count = count + 1
1428
+
1369
1429
  if await process_contact_chat_line(mc, contact, line):
1370
1430
  pass
1371
1431
 
@@ -1390,6 +1450,9 @@ async def apply_command_to_contacts(mc, contact_filter, line):
1390
1450
  else:
1391
1451
  logger.error(f"Can't send {line} to {contact['adv_name']}")
1392
1452
 
1453
+ if not json_output:
1454
+ print(f"> {count} matches in contacts")
1455
+
1393
1456
  async def send_cmd (mc, contact, cmd) :
1394
1457
  res = await mc.commands.send_cmd(contact, cmd)
1395
1458
  if not res is None and not res.type == EventType.ERROR:
@@ -1431,7 +1494,8 @@ async def send_msg (mc, contact, msg) :
1431
1494
  return res
1432
1495
 
1433
1496
  async def msg_ack (mc, contact, msg) :
1434
- timeout = 0 if not 'timeout' in contact else contact['timeout']
1497
+ timeout = 0 if not isinstance(contact, dict) or not 'timeout' in contact\
1498
+ else contact['timeout']
1435
1499
  res = await mc.commands.send_msg_with_retry(contact, msg,
1436
1500
  max_attempts=msg_ack.max_attempts,
1437
1501
  flood_after=msg_ack.flood_after,
@@ -1780,7 +1844,7 @@ async def next_cmd(mc, cmds, json_output=False):
1780
1844
 
1781
1845
  case "apply_to"|"at":
1782
1846
  argnum = 2
1783
- await apply_command_to_contacts(mc, cmds[1], cmds[2])
1847
+ await apply_command_to_contacts(mc, cmds[1], cmds[2], json_output=json_output)
1784
1848
 
1785
1849
  case "set":
1786
1850
  argnum = 2
@@ -2271,7 +2335,7 @@ async def next_cmd(mc, cmds, json_output=False):
2271
2335
  argnum = 2
2272
2336
  dest = None
2273
2337
 
2274
- if len(cmds[1]) == 12: # possibly an hex prefix
2338
+ if len(cmds[1]) >= 12: # possibly an hex prefix
2275
2339
  try:
2276
2340
  dest = bytes.fromhex(cmds[1])
2277
2341
  except ValueError:
@@ -2532,20 +2596,16 @@ async def next_cmd(mc, cmds, json_output=False):
2532
2596
  await mc.ensure_contacts()
2533
2597
  print(f"Discovered {len(dn)} nodes:")
2534
2598
  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:
2599
+ try :
2600
+ name = f"{n['pubkey'][0:2]} {mc.get_contact_by_key_prefix(n['pubkey'])['adv_name']}"
2601
+ except TypeError:
2537
2602
  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}")
2603
+ if n['node_type'] >= len(CONTACT_TYPENAMES):
2604
+ type = f"t:{n['node_type']}"
2605
+ else:
2606
+ type = CONTACT_TYPENAMES[n['node_type']]
2607
+
2608
+ print(f" {name:22} {type:>4} SNR: {n['SNR_in']:6,.2f}->{n['SNR']:6,.2f} RSSI: ->{n['RSSI']:4}")
2549
2609
 
2550
2610
  case "req_telemetry"|"rt" :
2551
2611
  argnum = 1
@@ -2561,7 +2621,7 @@ async def next_cmd(mc, cmds, json_output=False):
2561
2621
  else :
2562
2622
  print(json.dumps({
2563
2623
  "name": contact["adv_name"],
2564
- "pubkey_pre": contact["public_key"][0:12],
2624
+ "pubkey_pre": contact["public_key"][0:16],
2565
2625
  "lpp": res,
2566
2626
  }, indent = 4))
2567
2627
 
@@ -2695,7 +2755,13 @@ async def next_cmd(mc, cmds, json_output=False):
2695
2755
  print(json.dumps(res, indent=4))
2696
2756
  else :
2697
2757
  for c in res.items():
2698
- print(c[1]["adv_name"])
2758
+ if c[1]['out_path_len'] == -1:
2759
+ path_str = "Flood"
2760
+ elif c[1]['out_path_len'] == 0:
2761
+ path_str = "0 hop"
2762
+ else:
2763
+ path_str = f"{c[1]['out_path']}"
2764
+ print(f"{c[1]['adv_name']:30} {CONTACT_TYPENAMES[c[1]['type']]:4} {c[1]['public_key'][:12]}  {path_str}")
2699
2765
  print(f"> {len(mc.contacts)} contacts in device")
2700
2766
 
2701
2767
  case "reload_contacts" | "rc":
@@ -2763,7 +2829,7 @@ async def next_cmd(mc, cmds, json_output=False):
2763
2829
  if (path_len == 0) :
2764
2830
  print("0 hop")
2765
2831
  elif (path_len == -1) :
2766
- print("Path not set")
2832
+ print("Flood")
2767
2833
  else:
2768
2834
  print(path)
2769
2835
 
@@ -2790,6 +2856,8 @@ async def next_cmd(mc, cmds, json_output=False):
2790
2856
  print(f"Unknown contact {cmds[1]}")
2791
2857
  else:
2792
2858
  path = cmds[2].replace(",","") # we'll accept path with ,
2859
+ if path == "0":
2860
+ path = ""
2793
2861
  try:
2794
2862
  res = await mc.commands.change_contact_path(contact, path)
2795
2863
  logger.debug(res)
@@ -3178,11 +3246,16 @@ def command_help():
3178
3246
  def usage () :
3179
3247
  """ Prints some help """
3180
3248
  version()
3249
+ command_usage()
3250
+ print(" Available Commands and shorcuts (can be chained) :""")
3251
+ command_help()
3252
+
3253
+ def command_usage() :
3181
3254
  print("""
3182
3255
  Usage : meshcore-cli <args> <commands>
3183
3256
 
3184
3257
  Arguments :
3185
- -h : prints this help
3258
+ -h : prints help for arguments and commands
3186
3259
  -v : prints version
3187
3260
  -j : json output (disables init file)
3188
3261
  -D : debug
@@ -3198,9 +3271,7 @@ def usage () :
3198
3271
  -b <baudrate> : specify baudrate
3199
3272
  -C : toggles classic mode for prompt
3200
3273
  -c <on/off> : disables most of color output if off
3201
-
3202
- Available Commands and shorcuts (can be chained) :""")
3203
- command_help()
3274
+ """)
3204
3275
 
3205
3276
  def get_help_for (cmdname, context="line") :
3206
3277
  if cmdname == "apply_to" or cmdname == "at" :
@@ -3213,7 +3284,7 @@ def get_help_for (cmdname, context="line") :
3213
3284
  - d, direct, similar to h>-1
3214
3285
  - f, flood, similar to h<0 or h=-1
3215
3286
 
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 ...
3287
+ 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
3288
 
3218
3289
  Examples:
3219
3290
  # removes all clients that have not been updated in last 2 days
@@ -3252,7 +3323,8 @@ def get_help_for (cmdname, context="line") :
3252
3323
  print_new_contacts : display new pending contacts when available
3253
3324
  print_path_updates : display path updates as they come
3254
3325
  custom : all custom variables in json format
3255
- each custom var can also be get/set directly""")
3326
+ each custom var can also be get/set directly
3327
+ """)
3256
3328
 
3257
3329
  elif cmdname == "set" :
3258
3330
  print("""Available parameters :
@@ -3265,12 +3337,15 @@ def get_help_for (cmdname, context="line") :
3265
3337
  lat <lat> : latitude
3266
3338
  lon <lon> : longitude
3267
3339
  coords <lat,lon> : coordinates
3268
- auto_update_contacts <> : automatically updates contact list
3269
3340
  multi_ack <on/off> : multi-acks feature
3270
3341
  telemetry_mode_base <mode> : set basic telemetry mode all/selected/off
3271
3342
  telemetry_mode_loc <mode> : set location telemetry mode all/selected/off
3272
3343
  telemetry_mode_env <mode> : set env telemetry mode all/selected/off
3273
3344
  advert_loc_policy <policy> : "share" means loc will be shared in adv
3345
+ manual_add_contacts <on/off>: let user manually add contacts to device
3346
+ - when off device automatically adds contacts from adverts
3347
+ - when on contacts must be added manually using add_pending
3348
+ (pending contacts list is built by meshcli from adverts while connected)
3274
3349
  display:
3275
3350
  print_snr <on/off> : toggle snr display in messages
3276
3351
  print_adverts <on/off> : display adverts as they come
@@ -3280,12 +3355,14 @@ def get_help_for (cmdname, context="line") :
3280
3355
  channel_echoes <on/off> : print repeats for channel data
3281
3356
  echo_unk_channels <on/off> : also dump unk channels (encrypted)
3282
3357
  color <on/off> : color off should remove ANSI codes from output
3283
- prompt:
3358
+ meshcore-cli behaviour:
3284
3359
  classic_prompt <on/off> : activates less fancier prompt
3285
3360
  arrow_head <string> : change arrow head in prompt
3286
3361
  slash_start <string> : idem for slash start
3287
3362
  slash_end <string> : slash end
3288
- invert_slash <on/off> : apply color inversion to slash """)
3363
+ invert_slash <on/off> : apply color inversion to slash
3364
+ auto_update_contacts <on/of>: auto sync contact list with device
3365
+ """)
3289
3366
 
3290
3367
  elif cmdname == "scope":
3291
3368
  print("""scope <scope> : changes flood scope of the node
@@ -3296,7 +3373,31 @@ Managing Flood Scope in interactive mode
3296
3373
  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
3374
  When entering chat mode, scope will be reset to *, meaning classic flood.
3298
3375
  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.""")
3376
+ 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.
3377
+ """)
3378
+
3379
+ elif cmdname == "contact_info":
3380
+ print("""contact_info <ct> : displays contact info
3381
+
3382
+ in interactive mode, there are some lighter commands that can be chained to give more compact information
3383
+ - contact_name (cn)
3384
+ - contact_key (ck)
3385
+ - contact_type (ct)
3386
+ """)
3387
+
3388
+ elif cmdname == "pending_contacts" or cmdname == "flush_pending" or cmdname == "add_pending":
3389
+ print("""Contact management
3390
+
3391
+ To receive a message from another user, it is necessary to have its public key. This key is stored on a contact list in the device, and this list has a finite size (50 when meshcore started, now over 350 for most devices).
3392
+
3393
+ By default contacts are automatically added to the device contact list when an advertisement is received, so as soon as you receive an advert, you can talk with your buddy.
3394
+
3395
+ With growing number of users, it becomes necessary to manage contact list and one of the ways is to add contacts manually to the device. This is done by turning on manual_add_contacts. Once this option has been turned on, a pending list is built by meshcore-cli from the received adverts. You can view the list issuing a pending_contacts command, flush the list using flush_pending or add a contact from the list with add_pending followed by the key of the contact or its name (both will be auto-completed with tab).
3396
+
3397
+ This feature only really works in interactive mode.
3398
+
3399
+ Note: There is also an auto_update_contacts setting that has nothing to do with adding contacts, it permits to automatically sync contact lists between device and meshcore-cli (when there is an update in name, location or path).
3400
+ """)
3300
3401
 
3301
3402
  else:
3302
3403
  print(f"Sorry, no help yet for {cmdname}")
@@ -3313,13 +3414,19 @@ async def main(argv):
3313
3414
  baudrate = 115200
3314
3415
  timeout = 2
3315
3416
  pin = None
3417
+ first_device = False
3316
3418
  # If there is an address in config file, use it by default
3317
3419
  # unless an arg is explicitely given
3318
3420
  if os.path.exists(MCCLI_ADDRESS) :
3319
3421
  with open(MCCLI_ADDRESS, encoding="utf-8") as f :
3320
3422
  address = f.readline().strip()
3321
3423
 
3322
- opts, args = getopt.getopt(argv, "a:d:s:ht:p:b:fjDhvSlT:Pc:C")
3424
+ try:
3425
+ opts, args = getopt.getopt(argv, "a:d:s:ht:p:b:fjDhvSlT:Pc:C")
3426
+ except getopt.GetoptError:
3427
+ print("Unrecognized option, use -h to get more help")
3428
+ command_usage()
3429
+ return
3323
3430
  for opt, arg in opts :
3324
3431
  match opt:
3325
3432
  case "-c" :
@@ -3356,6 +3463,7 @@ async def main(argv):
3356
3463
  return
3357
3464
  case "-f": # connect to first encountered device
3358
3465
  address = ""
3466
+ first_device = True
3359
3467
  case "-l" :
3360
3468
  print("BLE devices:")
3361
3469
  try :
@@ -3365,7 +3473,7 @@ async def main(argv):
3365
3473
  for d in devices :
3366
3474
  if not d.name is None and d.name.startswith("MeshCore-"):
3367
3475
  print(f" {d.address} {d.name}")
3368
- except BleakError:
3476
+ except (BleakError, BleakDBusError):
3369
3477
  print(" No BLE HW")
3370
3478
  print("\nSerial ports:")
3371
3479
  ports = serial.tools.list_ports.comports()
@@ -3380,7 +3488,7 @@ async def main(argv):
3380
3488
  for d in devices:
3381
3489
  if not d.name is None and d.name.startswith("MeshCore-"):
3382
3490
  choices.append(({"type":"ble","device":d}, f"{d.address:<22} {d.name}"))
3383
- except BleakError:
3491
+ except (BleakError, BleakDBusError):
3384
3492
  logger.info("No BLE Device")
3385
3493
 
3386
3494
  ports = serial.tools.list_ports.comports()
@@ -3451,8 +3559,15 @@ async def main(argv):
3451
3559
 
3452
3560
  try :
3453
3561
  mc = await MeshCore.create_ble(address=address, device=device, client=client, debug=debug, only_error=json_output, pin=pin)
3562
+ except (BleakError, BleakDBusError):
3563
+ print("BLE connection asked (default behaviour), but no BLE HW found")
3564
+ print("Call meshcore-cli with -h for some more help (on commands)")
3565
+ command_usage()
3566
+ return
3454
3567
  except ConnectionError :
3455
3568
  logger.info("Error while connecting, retrying once ...")
3569
+ if first_device :
3570
+ address = "" # reset address to change device if first_device was asked
3456
3571
  if device is None and client is None: # Search for device
3457
3572
  logger.info(f"Scanning BLE for device matching {address}")
3458
3573
  devices = await BleakScanner.discover(timeout=timeout)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcore-cli
3
- Version: 1.3.0
3
+ Version: 1.3.7
4
4
  Summary: Command line interface to meshcore companion radios
5
5
  Project-URL: Homepage, https://github.com/fdlamotte/meshcore-cli
6
6
  Project-URL: Issues, https://github.com/fdlamotte/meshcore-cli/issues
@@ -10,7 +10,8 @@ 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.24
13
+ Requires-Dist: bleak<2.0,>=0.22
14
+ Requires-Dist: meshcore>=2.2.2
14
15
  Requires-Dist: prompt-toolkit>=3.0.50
15
16
  Requires-Dist: pycryptodome
16
17
  Requires-Dist: requests>=2.28.0
@@ -198,6 +199,18 @@ f1down/#fdl|*> 8
198
199
  #fdl f1down: 8 [2521] 1.00-109
199
200
  ```
200
201
 
202
+ ### Contact management
203
+
204
+ To receive a message from another user, it is necessary to have its public key. This key is stored on a contact list in the device, and this list has a finite size (50 when meshcore started, now over 350 for most devices).
205
+
206
+ By default contacts are automatically added to the device contact list when an advertisement is received, so as soon as you receive an advert, you can talk with your buddy.
207
+
208
+ With growing number of users, it becomes necessary to manage contact list and one of the ways is to add contacts manually to the device. This is done by turning on `manual_add_contacts`. Once this option has been turned on, a pending list is built by meshcore-cli from the received adverts. You can view the list issuing a `pending_contacts` command, flush the list using `flush_pending` or add a contact from the list with `add_pending` followed by the key of the contact or its name (both will be auto-completed with tab).
209
+
210
+ This feature only really works in interactive mode.
211
+
212
+ Note: There is also an `auto_update_contacts` setting that has nothing to do with adding contacts, it permits to automatically sync contact lists between device and meshcore-cli (when there is an update in name, location or path).
213
+
201
214
  ### Issuing batch commands to contacts with apply to
202
215
 
203
216
  `apply_to <f> <cmd>` : applies cmd to contacts matching filter `<f>` it can be used to apply the same command to a pool of repeaters, or remove some contacts matching a condition.
@@ -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=iRrICDi9pCAngyuY6_Jc6teubgi4gomJ8l_NHXYNFrE,151869
4
+ meshcore_cli-1.3.7.dist-info/METADATA,sha256=0o6ju0KlttNIOFoy3_0-rUEIlVwSymgsHDLkkr-So5w,17137
5
+ meshcore_cli-1.3.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
6
+ meshcore_cli-1.3.7.dist-info/entry_points.txt,sha256=77V29Pyth11GteDk7tneBN3MMk8JI7bTlS-BGSmxCmI,103
7
+ meshcore_cli-1.3.7.dist-info/licenses/LICENSE,sha256=F9s987VtS0AKxW7LdB2EkLMkrdeERI7ICdLJR60A9M4,1066
8
+ meshcore_cli-1.3.7.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=C6dZGtfqIEIsEaoVBNOmr4xmoy0sMr-gZOTqZ0prxd0,146609
4
- meshcore_cli-1.3.0.dist-info/METADATA,sha256=Fgv1cb5iij3ExDitwB8vRvP7GuxSOPqkv5nxnyATMRA,15873
5
- meshcore_cli-1.3.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
6
- meshcore_cli-1.3.0.dist-info/entry_points.txt,sha256=77V29Pyth11GteDk7tneBN3MMk8JI7bTlS-BGSmxCmI,103
7
- meshcore_cli-1.3.0.dist-info/licenses/LICENSE,sha256=F9s987VtS0AKxW7LdB2EkLMkrdeERI7ICdLJR60A9M4,1066
8
- meshcore_cli-1.3.0.dist-info/RECORD,,