meshcore-cli 1.2.14__py3-none-any.whl → 1.3.1__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.2.14"
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 = ""
@@ -205,27 +209,57 @@ process_event_message.last_node=None
205
209
 
206
210
  async def handle_log_rx(event):
207
211
  mc = handle_log_rx.mc
208
- if handle_log_rx.json_log_rx: # json mode ... raw dump
209
- msg = json.dumps(event.payload)
210
- if handle_message.above:
211
- print_above(msg)
212
- else :
213
- print(msg)
214
- return
215
212
 
216
213
  pkt = bytes().fromhex(event.payload["payload"])
217
214
  pbuf = io.BytesIO(pkt)
218
215
  header = pbuf.read(1)[0]
216
+ route_type = header & 0x03
217
+ payload_type = (header & 0x3c) >> 2
218
+ payload_ver = (header & 0xc0) >> 6
219
+
220
+ transport_code = None
221
+ if route_type == 0x00 or route_type == 0x03: # has transport code
222
+ transport_code = pbuf.read(4) # discard transport code
223
+
224
+ path_len = pbuf.read(1)[0]
225
+ path = pbuf.read(path_len).hex() # Beware of traces where pathes are mixed
226
+
227
+ try :
228
+ route_typename = ROUTE_TYPENAMES[route_type]
229
+ except IndexError:
230
+ logger.debug(f"Unknown route type {route_type}")
231
+ route_typename = "UNK"
232
+
233
+ try :
234
+ payload_typename = PAYLOAD_TYPENAMES[payload_type]
235
+ except IndexError:
236
+ logger.debug(f"Unknown payload type {payload_type}")
237
+ payload_typename = "UNK"
238
+
239
+ pkt_payload = pbuf.read()
240
+
241
+ event.payload["header"] = header
242
+ event.payload["route_type"] = route_type
243
+ event.payload["route_typename"] = route_typename
244
+ event.payload["payload_type"] = payload_type
245
+ event.payload["payload_typename"]= payload_typename
219
246
 
220
- if header & ~1 == 0x14: # flood msg / channel
247
+ event.payload["payload_ver"] = payload_ver
248
+
249
+ if not transport_code is None:
250
+ event.payload["transport_code"] = transport_code.hex()
251
+
252
+ event.payload["path_len"] = path_len
253
+ event.payload["path"] = path
254
+
255
+ event.payload["pkt_payload"] = pkt_payload.hex()
256
+
257
+ if payload_type == 0x05: # flood msg / channel
221
258
  if handle_log_rx.channel_echoes:
222
- if header & 1 == 0: # has transport code
223
- pbuf.read(4) # discard transport code
224
- path_len = pbuf.read(1)[0]
225
- path = pbuf.read(path_len).hex()
226
- chan_hash = pbuf.read(1).hex()
227
- cipher_mac = pbuf.read(2)
228
- msg = pbuf.read() # until the end of buffer
259
+ pk_buf = io.BytesIO(pkt_payload)
260
+ chan_hash = pk_buf.read(1).hex()
261
+ cipher_mac = pk_buf.read(2)
262
+ msg = pk_buf.read() # until the end of buffer
229
263
 
230
264
  channel = None
231
265
  for c in await get_channels(mc):
@@ -258,6 +292,14 @@ async def handle_log_rx(event):
258
292
  else:
259
293
  print(txt)
260
294
 
295
+ if handle_log_rx.json_log_rx: # json mode ... raw dump
296
+ msg = json.dumps(event.payload)
297
+ if handle_message.above:
298
+ print_above(msg)
299
+ else :
300
+ print(msg)
301
+
302
+
261
303
  handle_log_rx.json_log_rx = False
262
304
  handle_log_rx.channel_echoes = False
263
305
  handle_log_rx.mc = None
@@ -390,7 +432,7 @@ class MyNestedCompleter(NestedCompleter):
390
432
  opts = self.options.keys()
391
433
  completer = WordCompleter(
392
434
  opts, ignore_case=self.ignore_case,
393
- 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\#\?]+)"))
394
436
  yield from completer.get_completions(document, complete_event)
395
437
  else: # normal behavior for remainder
396
438
  yield from super().get_completions(document, complete_event)
@@ -541,11 +583,21 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
541
583
  "flood_after":None,
542
584
  "custom":None,
543
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,
544
594
  }
545
595
 
546
596
  contact_completion_list = {
547
597
  "contact_info": None,
548
598
  "contact_name": None,
599
+ "contact_key": None,
600
+ "contact_type": None,
549
601
  "contact_lastmod": None,
550
602
  "export_contact" : None,
551
603
  "share_contact" : None,
@@ -708,7 +760,7 @@ make_completion_dict.custom_vars = {}
708
760
  async def interactive_loop(mc, to=None) :
709
761
  print("""Interactive mode, most commands from terminal chat should work.
710
762
  Use \"to\" to select recipient, use Tab to complete name ...
711
- Line starting with \"$\" or \".\" will issue a meshcli command.
763
+ Some cmds have an help accessible with ?<cmd>. Do ?[Tab] to get a list.
712
764
  \"quit\", \"q\", CTRL+D will end interactive mode""")
713
765
 
714
766
  contact = to
@@ -1061,6 +1113,26 @@ async def process_contact_chat_line(mc, contact, line):
1061
1113
  await process_cmds(mc, args)
1062
1114
  return True
1063
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
+
1064
1136
  if line.startswith("contact_name") or line.startswith("cn"):
1065
1137
  print(contact['adv_name'],end="")
1066
1138
  if " " in line:
@@ -1071,6 +1143,44 @@ async def process_contact_chat_line(mc, contact, line):
1071
1143
  print("")
1072
1144
  return True
1073
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
+
1161
+ if line.startswith("sleep") or line.startswith("s"):
1162
+ try:
1163
+ sleeptime = int(line.split(" ",2)[1])
1164
+ cmd_pos = 2
1165
+ except IndexError: # nothing arg after sleep
1166
+ sleeptime = 1
1167
+ cmd_pos = 0
1168
+ except ValueError:
1169
+ sleeptime = 1
1170
+ cmd_pos = 1
1171
+
1172
+ try:
1173
+ if cmd_pos > 0:
1174
+ secline = line.split(" ",cmd_pos)[cmd_pos]
1175
+ await process_contact_chat_line(mc, contact, secline)
1176
+ except IndexError:
1177
+ pass
1178
+
1179
+ # will sleep after executed command if there is a command
1180
+ await asyncio.sleep(sleeptime)
1181
+
1182
+ return True
1183
+
1074
1184
  if line == "contact_lastmod":
1075
1185
  timestamp = contact["lastmod"]
1076
1186
  print(f"{contact['adv_name']} updated"
@@ -1079,20 +1189,24 @@ async def process_contact_chat_line(mc, contact, line):
1079
1189
  return True
1080
1190
 
1081
1191
  # commands that take contact as second arg will be sent to recipient
1082
- if line == "sc" or line == "share_contact" or\
1083
- line == "ec" or line == "export_contact" or\
1084
- line == "uc" or line == "upload_contact" or\
1085
- line == "rp" or line == "reset_path" or\
1086
- line == "dp" or line == "disc_path" or\
1087
- line == "contact_info" or line == "ci" or\
1088
- line == "req_status" or line == "rs" or\
1089
- line == "req_neighbours" or line == "rn" or\
1090
- line == "req_telemetry" or line == "rt" or\
1091
- line == "req_acl" or\
1092
- line == "path" or\
1093
- line == "logout" :
1094
- 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']]
1095
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)
1096
1210
  return True
1097
1211
 
1098
1212
  # special case for rp that can be chained from cmdline
@@ -1242,12 +1356,13 @@ async def process_contact_chat_line(mc, contact, line):
1242
1356
 
1243
1357
  return False
1244
1358
 
1245
- async def apply_command_to_contacts(mc, contact_filter, line):
1359
+ async def apply_command_to_contacts(mc, contact_filter, line, json_output=False):
1246
1360
  upd_before = None
1247
1361
  upd_after = None
1248
1362
  contact_type = None
1249
1363
  min_hops = None
1250
1364
  max_hops = None
1365
+ count = 0
1251
1366
 
1252
1367
  await mc.ensure_contacts()
1253
1368
 
@@ -1302,6 +1417,9 @@ async def apply_command_to_contacts(mc, contact_filter, line):
1302
1417
  (upd_after is None or contact["lastmod"] > upd_after) and\
1303
1418
  (min_hops is None or contact["out_path_len"] >= min_hops) and\
1304
1419
  (max_hops is None or contact["out_path_len"] <= max_hops):
1420
+
1421
+ count = count + 1
1422
+
1305
1423
  if await process_contact_chat_line(mc, contact, line):
1306
1424
  pass
1307
1425
 
@@ -1326,6 +1444,9 @@ async def apply_command_to_contacts(mc, contact_filter, line):
1326
1444
  else:
1327
1445
  logger.error(f"Can't send {line} to {contact['adv_name']}")
1328
1446
 
1447
+ if not json_output:
1448
+ print(f"> {count} matches in contacts")
1449
+
1329
1450
  async def send_cmd (mc, contact, cmd) :
1330
1451
  res = await mc.commands.send_cmd(contact, cmd)
1331
1452
  if not res is None and not res.type == EventType.ERROR:
@@ -1716,26 +1837,14 @@ async def next_cmd(mc, cmds, json_output=False):
1716
1837
 
1717
1838
  case "apply_to"|"at":
1718
1839
  argnum = 2
1719
- 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)
1720
1841
 
1721
1842
  case "set":
1722
1843
  argnum = 2
1723
1844
  match cmds[1]:
1724
1845
  case "help" :
1725
1846
  argnum = 1
1726
- print("""Available parameters :
1727
- pin <pin> : ble pin
1728
- radio <freq,bw,sf,cr> : radio params
1729
- tuning <rx_dly,af> : tuning params
1730
- tx <dbm> : tx power
1731
- name <name> : node name
1732
- lat <lat> : latitude
1733
- lon <lon> : longitude
1734
- coords <lat,lon> : coordinates
1735
- print_snr <on/off> : toggle snr display in messages
1736
- print_adverts <on/off> : display adverts as they come
1737
- print_new_contacts <on/off> : display new pending contacts when available
1738
- print_path_updates <on/off> : display path updates as they come""")
1847
+ get_help_for("set")
1739
1848
  case "max_flood_attempts":
1740
1849
  msg_ack.max_flood_attempts=int(cmds[2])
1741
1850
  case "max_attempts":
@@ -1959,21 +2068,7 @@ async def next_cmd(mc, cmds, json_output=False):
1959
2068
  argnum = 1
1960
2069
  match cmds[1]:
1961
2070
  case "help":
1962
- print("""Gets parameters from node
1963
- name : node name
1964
- bat : battery level in mV
1965
- fstats : fs statistics
1966
- coords : adv coordinates
1967
- lat : latitude
1968
- lon : longitude
1969
- radio : radio parameters
1970
- tx : tx power
1971
- print_snr : snr display in messages
1972
- print_adverts : display adverts as they come
1973
- print_new_contacts : display new pending contacts when available
1974
- print_path_updates : display path updates as they come
1975
- custom : all custom variables in json format
1976
- each custom var can also be get/set directly""")
2071
+ get_help_for("get")
1977
2072
  case "max_flood_attempts":
1978
2073
  if json_output :
1979
2074
  print(json.dumps({"max_flood_attempts" : msg_ack.max_flood_attempts}))
@@ -2340,6 +2435,7 @@ async def next_cmd(mc, cmds, json_output=False):
2340
2435
  else :
2341
2436
  color = process_event_message.color
2342
2437
  classic = interactive_loop.classic or not color
2438
+ print(" ", end="")
2343
2439
  for t in ev.payload["path"]:
2344
2440
  if classic :
2345
2441
  print("→",end="")
@@ -2493,18 +2589,14 @@ async def next_cmd(mc, cmds, json_output=False):
2493
2589
  await mc.ensure_contacts()
2494
2590
  print(f"Discovered {len(dn)} nodes:")
2495
2591
  for n in dn:
2496
- name = f"{n['pubkey'][0:2]} {mc.get_contact_by_key_prefix(n['pubkey'])['adv_name']}"
2497
- 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:
2498
2595
  name = n["pubkey"][0:16]
2499
- type = f"t:{n['node_type']}"
2500
- if n['node_type'] == 1:
2501
- type = "CLI"
2502
- elif n['node_type'] == 2:
2503
- type = "REP"
2504
- elif n['node_type'] == 3:
2505
- type = "ROOM"
2506
- elif n['node_type'] == 4:
2507
- 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']]
2508
2600
 
2509
2601
  print(f" {name:16} {type:>4} SNR: {n['SNR_in']:6,.2f}->{n['SNR']:6,.2f} RSSI: ->{n['RSSI']:4}")
2510
2602
 
@@ -2522,7 +2614,7 @@ async def next_cmd(mc, cmds, json_output=False):
2522
2614
  else :
2523
2615
  print(json.dumps({
2524
2616
  "name": contact["adv_name"],
2525
- "pubkey_pre": contact["public_key"][0:12],
2617
+ "pubkey_pre": contact["public_key"][0:16],
2526
2618
  "lpp": res,
2527
2619
  }, indent = 4))
2528
2620
 
@@ -2610,15 +2702,30 @@ async def next_cmd(mc, cmds, json_output=False):
2610
2702
  if json_output:
2611
2703
  print(json.dumps(res, indent=4))
2612
2704
  else:
2705
+ width = os.get_terminal_size().columns
2613
2706
  print(f"Got {res['results_count']} neighbours out of {res['neighbours_count']} from {contact['adv_name']}:")
2614
2707
  for n in res['neighbours']:
2615
2708
  ct = mc.get_contact_by_key_prefix(n["pubkey"])
2616
- if ct :
2709
+ if ct and width > 60 :
2617
2710
  name = f"[{n['pubkey'][0:8]}] {ct['adv_name']}"
2711
+ name = f"{name:30}"
2712
+ elif ct :
2713
+ name = f"{ct['adv_name']}"
2714
+ name = f"{name:20}"
2618
2715
  else:
2619
2716
  name = f"[{n['pubkey']}]"
2620
2717
 
2621
- print(f" {name:30} last viewed {n['secs_ago']} sec ago at {n['snr']} ")
2718
+ t_s = n['secs_ago']
2719
+ time_ago = f"{t_s}s"
2720
+ if t_s / 86400 >= 1 : # result in days
2721
+ time_ago = f"{int(t_s/86400)}d ago{f' ({time_ago})' if width > 62 else ''}"
2722
+ elif t_s / 3600 >= 1 : # result in days
2723
+ time_ago = f"{int(t_s/3600)}h ago{f' ({time_ago})' if width > 62 else ''}"
2724
+ elif t_s / 60 >= 1 : # result in min
2725
+ time_ago = f"{int(t_s/60)}m ago{f' ({time_ago})' if width > 62 else ''}"
2726
+
2727
+
2728
+ print(f" {name} {time_ago}, {n['snr']}dB{' SNR' if width > 66 else ''}")
2622
2729
 
2623
2730
  case "req_binary" :
2624
2731
  argnum = 2
@@ -2641,7 +2748,13 @@ async def next_cmd(mc, cmds, json_output=False):
2641
2748
  print(json.dumps(res, indent=4))
2642
2749
  else :
2643
2750
  for c in res.items():
2644
- 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}")
2645
2758
  print(f"> {len(mc.contacts)} contacts in device")
2646
2759
 
2647
2760
  case "reload_contacts" | "rc":
@@ -2709,7 +2822,7 @@ async def next_cmd(mc, cmds, json_output=False):
2709
2822
  if (path_len == 0) :
2710
2823
  print("0 hop")
2711
2824
  elif (path_len == -1) :
2712
- print("Path not set")
2825
+ print("Flood")
2713
2826
  else:
2714
2827
  print(path)
2715
2828
 
@@ -2736,6 +2849,8 @@ async def next_cmd(mc, cmds, json_output=False):
2736
2849
  print(f"Unknown contact {cmds[1]}")
2737
2850
  else:
2738
2851
  path = cmds[2].replace(",","") # we'll accept path with ,
2852
+ if path == "0":
2853
+ path = ""
2739
2854
  try:
2740
2855
  res = await mc.commands.change_contact_path(contact, path)
2741
2856
  logger.debug(res)
@@ -3068,8 +3183,8 @@ def command_help():
3068
3183
  reboot : reboots node
3069
3184
  sleep <secs> : sleeps for a given amount of secs s
3070
3185
  wait_key : wait until user presses <Enter> wk
3071
- apply_to <scope> <cmds>: sends cmds to contacts matching scope at
3072
- Messenging
3186
+ apply_to <f> <cmds> : sends cmds to contacts matching f at
3187
+ Messaging
3073
3188
  msg <name> <msg> : send message to node by name m {
3074
3189
  wait_ack : wait an ack wa }
3075
3190
  chan <nb> <msg> : send message to channel number <nb> ch
@@ -3082,6 +3197,7 @@ def command_help():
3082
3197
  get_channel <n> : get info for channel (by number or name)
3083
3198
  set_channel n nm k : set channel info (nb, name, key)
3084
3199
  remove_channel <n> : remove channel (by number or name)
3200
+ scope <s> : sets scope for flood messages
3085
3201
  Management
3086
3202
  advert : sends advert a
3087
3203
  floodadv : flood advert
@@ -3133,8 +3249,6 @@ def usage () :
3133
3249
  -D : debug
3134
3250
  -S : scan for devices and show a selector
3135
3251
  -l : list available ble/serial devices and exit
3136
- -C : toggles classic mode for prompt
3137
- -c <on/off> : disables most of color output if off
3138
3252
  -T <timeout> : timeout for the ble scan (-S and -l) default 2s
3139
3253
  -a <address> : specifies device address (can be a name)
3140
3254
  -d <name> : filter meshcore devices with name or address
@@ -3143,14 +3257,16 @@ def usage () :
3143
3257
  -p <port> : specifies tcp port (default 5000)
3144
3258
  -s <port> : use serial port <port>
3145
3259
  -b <baudrate> : specify baudrate
3260
+ -C : toggles classic mode for prompt
3261
+ -c <on/off> : disables most of color output if off
3146
3262
 
3147
3263
  Available Commands and shorcuts (can be chained) :""")
3148
3264
  command_help()
3149
3265
 
3150
3266
  def get_help_for (cmdname, context="line") :
3151
3267
  if cmdname == "apply_to" or cmdname == "at" :
3152
- print("""apply_to <scope> <cmd> : applies cmd to contacts matching scope
3153
- Scope acts like a filter with comma separated fields :
3268
+ print("""apply_to <f> <cmd> : applies cmd to contacts matching filter <f>
3269
+ Filter is constructed with comma separated fields :
3154
3270
  - u, matches modification time < or > than a timestamp
3155
3271
  (can also be days hours or minutes ago if followed by d,h or m)
3156
3272
  - t, matches the type (1: client, 2: repeater, 3: room, 4: sensor)
@@ -3158,7 +3274,7 @@ def get_help_for (cmdname, context="line") :
3158
3274
  - d, direct, similar to h>-1
3159
3275
  - f, flood, similar to h<0 or h=-1
3160
3276
 
3161
- Note: Some commands like contact_name (aka cn), reset_path (aka rp), forget_password (aka fp) can be chained.
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 ...
3162
3278
 
3163
3279
  Examples:
3164
3280
  # removes all clients that have not been updated in last 2 days
@@ -3169,7 +3285,7 @@ def get_help_for (cmdname, context="line") :
3169
3285
  at t=2 rp login
3170
3286
  """)
3171
3287
 
3172
- if cmdname == "node_discover" or cmdname == "nd" :
3288
+ elif cmdname == "node_discover" or cmdname == "nd" :
3173
3289
  print("""node_discover <filter> : discovers 0-hop nodes and displays signal info
3174
3290
 
3175
3291
  filter can be "all" for all types or nodes or a comma separated list consisting of :
@@ -3181,6 +3297,80 @@ def get_help_for (cmdname, context="line") :
3181
3297
  nd can be used with no filter parameter ... !!! BEWARE WITH CHAINING !!!
3182
3298
  """)
3183
3299
 
3300
+ elif cmdname == "get" :
3301
+ print("""Gets parameters from node
3302
+ Please see also help for set command, which is more up to date ...
3303
+ name : node name
3304
+ bat : battery level in mV
3305
+ fstats : fs statistics
3306
+ coords : adv coordinates
3307
+ lat : latitude
3308
+ lon : longitude
3309
+ radio : radio parameters
3310
+ tx : tx power
3311
+ print_snr : snr display in messages
3312
+ print_adverts : display adverts as they come
3313
+ print_new_contacts : display new pending contacts when available
3314
+ print_path_updates : display path updates as they come
3315
+ custom : all custom variables in json format
3316
+ each custom var can also be get/set directly
3317
+ """)
3318
+
3319
+ elif cmdname == "set" :
3320
+ print("""Available parameters :
3321
+ device:
3322
+ pin <pin> : ble pin
3323
+ radio <freq,bw,sf,cr> : radio params
3324
+ tuning <rx_dly,af> : tuning params
3325
+ tx <dbm> : tx power
3326
+ name <name> : node name
3327
+ lat <lat> : latitude
3328
+ lon <lon> : longitude
3329
+ coords <lat,lon> : coordinates
3330
+ auto_update_contacts <> : automatically updates contact list
3331
+ multi_ack <on/off> : multi-acks feature
3332
+ telemetry_mode_base <mode> : set basic telemetry mode all/selected/off
3333
+ telemetry_mode_loc <mode> : set location telemetry mode all/selected/off
3334
+ telemetry_mode_env <mode> : set env telemetry mode all/selected/off
3335
+ advert_loc_policy <policy> : "share" means loc will be shared in adv
3336
+ display:
3337
+ print_snr <on/off> : toggle snr display in messages
3338
+ print_adverts <on/off> : display adverts as they come
3339
+ print_new_contacts <on/off> : display new pending contacts when available
3340
+ print_path_updates <on/off> : display path updates as they come
3341
+ json_log_rx <on/off> : logs packets incoming to device as json
3342
+ channel_echoes <on/off> : print repeats for channel data
3343
+ echo_unk_channels <on/off> : also dump unk channels (encrypted)
3344
+ color <on/off> : color off should remove ANSI codes from output
3345
+ prompt:
3346
+ classic_prompt <on/off> : activates less fancier prompt
3347
+ arrow_head <string> : change arrow head in prompt
3348
+ slash_start <string> : idem for slash start
3349
+ slash_end <string> : slash end
3350
+ invert_slash <on/off> : apply color inversion to slash
3351
+ """)
3352
+
3353
+ elif cmdname == "scope":
3354
+ print("""scope <scope> : changes flood scope of the node
3355
+
3356
+ The scope command can be used from command line or interactive mode to set the region in which flood packets will be transmitted.
3357
+
3358
+ Managing Flood Scope in interactive mode
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.
3360
+ When entering chat mode, scope will be reset to *, meaning classic flood.
3361
+ You can switch scope using the scope command, or postfixing the to command with %<scope>.
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
+ """)
3373
+
3184
3374
  else:
3185
3375
  print(f"Sorry, no help yet for {cmdname}")
3186
3376
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcore-cli
3
- Version: 1.2.14
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
@@ -58,22 +58,25 @@ Init files can also be defined for a given device, meshcore-cli will look for `&
58
58
 
59
59
  ### Arguments
60
60
 
61
- Arguments mostly deals with ble connection
61
+ Arguments mostly deals with connection to the node
62
62
 
63
63
  <pre>
64
64
  -h : prints this help
65
65
  -v : prints version
66
66
  -j : json output (disables init file)
67
67
  -D : debug
68
- -S : performs a ble scan and ask for device
69
- -l : list available ble devices and exit
70
- -T &lt;timeout> : timeout for the ble scan (-S and -l) default 2s
71
- -a &lt;address> : specifies device address (can be a name)
72
- -d &lt;name> : filter meshcore devices with name or address
73
- -t &lt;hostname> : connects via tcp/ip
74
- -p &lt;port> : specifies tcp port (default 5000)
75
- -s &lt;port> : use serial port &lt;port>
76
- -b &lt;baudrate> : specify baudrate
68
+ -S : scan for devices and show a selector
69
+ -l : list available ble/serial devices and exit
70
+ -T &lt;timeout&gt; : timeout for the ble scan (-S and -l) default 2s
71
+ -a &lt;address&gt; : specifies device address (can be a name)
72
+ -d &lt;name&gt; : filter meshcore devices with name or address
73
+ -P : forces pairing via the OS
74
+ -t &lt;hostname&gt; : connects via tcp/ip
75
+ -p &lt;port&gt; : specifies tcp port (default 5000)
76
+ -s &lt;port&gt; : use serial port &lt;port&gt;
77
+ -b &lt;baudrate&gt; : specify baudrate
78
+ -C : toggles classic mode for prompt
79
+ -c &lt;on/off&gt; : disables most of color output if off
77
80
  </pre>
78
81
 
79
82
  ### Available Commands
@@ -81,60 +84,70 @@ Arguments mostly deals with ble connection
81
84
  Commands are given after arguments, they can be chained and some have shortcuts. Also prefixing a command with a dot `.` will force it to output json instead of synthetic result.
82
85
 
83
86
  <pre>
87
+ ?&lt;cmd&gt; may give you some more help about cmd
84
88
  General commands
85
89
  chat : enter the chat (interactive) mode
86
- chat_to &lt;ct> : enter chat with contact to
87
- script &lt;filename> : execute commands in filename
90
+ chat_to &lt;ct&gt; : enter chat with contact to
91
+ script &lt;filename&gt; : execute commands in filename
88
92
  infos : print informations about the node i
89
93
  self_telemetry : print own telemtry t
90
94
  card : export this node URI e
91
95
  ver : firmware version v
92
96
  reboot : reboots node
93
- sleep &lt;secs> : sleeps for a given amount of secs s
94
- wait_key : wait until user presses &lt;Enter> wk
95
- Messenging
96
- msg &lt;name> &lt;msg> : send message to node by name m {
97
+ sleep &lt;secs&gt; : sleeps for a given amount of secs s
98
+ wait_key : wait until user presses &lt;Enter&gt; wk
99
+ apply_to &lt;f&gt; &lt;cmds&gt; : sends cmds to contacts matching f at
100
+ Messaging
101
+ msg &lt;name&gt; &lt;msg&gt; : send message to node by name m {
97
102
  wait_ack : wait an ack wa }
98
- chan &lt;nb> &lt;msg> : send message to channel number &lt;nb> ch
99
- public &lt;msg> : send message to public channel (0) dch
103
+ chan &lt;nb&gt; &lt;msg&gt; : send message to channel number &lt;nb&gt; ch
104
+ public &lt;msg&gt; : send message to public channel (0) dch
100
105
  recv : reads next msg r
101
106
  wait_msg : wait for a message and read it wm
102
107
  sync_msgs : gets all unread msgs from the node sm
103
108
  msgs_subscribe : display msgs as they arrive ms
104
- get_channel &lt;n> : get info for channel n
109
+ get_channels : prints all channel info
110
+ get_channel &lt;n&gt; : get info for channel (by number or name)
105
111
  set_channel n nm k : set channel info (nb, name, key)
112
+ remove_channel &lt;n&gt; : remove channel (by number or name)
113
+ scope &lt;s&gt; : sets node's flood scope
106
114
  Management
107
115
  advert : sends advert a
108
116
  floodadv : flood advert
109
- get &lt;param> : gets a param, "get help" for more
110
- set &lt;param> &lt;value> : sets a param, "set help" for more
111
- time &lt;epoch> : sets time to given epoch
117
+ get &lt;param&gt; : gets a param, \"get help\" for more
118
+ set &lt;param&gt; &lt;value&gt; : sets a param, \"set help\" for more
119
+ time &lt;epoch&gt; : sets time to given epoch
112
120
  clock : get current time
113
121
  clock sync : sync device clock st
122
+ node_discover &lt;filter&gt; : discovers nodes based on their type nd
114
123
  Contacts
115
124
  contacts / list : gets contact list lc
116
- contact_info &lt;ct> : prints information for contact ct ci
117
- contact_timeout &lt;ct> v : sets temp default timeout for contact
118
- share_contact &lt;ct> : share a contact with others sc
119
- export_contact &lt;ct> : get a contact's URI ec
120
- import_contact &lt;URI> : import a contact from its URI ic
121
- remove_contact &lt;ct> : removes a contact from this node
122
- path &lt;ct> : diplays path for a contact
123
- reset_path &lt;ct> : resets path to a contact to flood rp
124
- change_path &lt;ct> &lt;pth> : change the path to a contact cp
125
- change_flags &lt;ct> &lt;f> : change contact flags (tel_l|tel_a|star)cf
126
- req_telemetry &lt;ct> : prints telemetry data as json rt
127
- req_mma &lt;ct> : requests min/max/avg for a sensor rm
128
- req_acl &lt;ct> : requests access control list for sensor
125
+ reload_contacts : force reloading all contacts rc
126
+ contact_info &lt;ct&gt; : prints information for contact ct ci
127
+ contact_timeout &lt;ct&gt; v : sets temp default timeout for contact
128
+ share_contact &lt;ct&gt; : share a contact with others sc
129
+ export_contact &lt;ct&gt; : get a contact's URI ec
130
+ import_contact &lt;URI&gt; : import a contact from its URI ic
131
+ remove_contact &lt;ct&gt; : removes a contact from this node
132
+ path &lt;ct&gt; : diplays path for a contact
133
+ disc_path &lt;ct&gt; : discover new path and display dp
134
+ reset_path &lt;ct&gt; : resets path to a contact to flood rp
135
+ change_path &lt;ct&gt; &lt;pth&gt; : change the path to a contact cp
136
+ change_flags &lt;ct&gt; &lt;f&gt; : change contact flags (tel_l|tel_a|star)cf
137
+ req_telemetry &lt;ct&gt; : prints telemetry data as json rt
138
+ req_mma &lt;ct&gt; : requests min/max/avg for a sensor rm
139
+ req_acl &lt;ct&gt; : requests access control list for sensor
129
140
  pending_contacts : show pending contacts
130
- add_pending &lt;key> : manually add pending contact from key
131
- flush_pending : flush pending contact clist
141
+ add_pending &lt;pending&gt; : manually add pending contact
142
+ flush_pending : flush pending contact list
132
143
  Repeaters
133
- login &lt;name> &lt;pwd> : log into a node (rep) with given pwd l
134
- logout &lt;name> : log out of a repeater
135
- cmd &lt;name> &lt;cmd> : sends a command to a repeater (no ack) c [
144
+ login &lt;name&gt; &lt;pwd&gt; : log into a node (rep) with given pwd l
145
+ logout &lt;name&gt; : log out of a repeater
146
+ cmd &lt;name&gt; &lt;cmd&gt; : sends a command to a repeater (no ack) c [
136
147
  wmt8 : wait for a msg (reply) with a timeout ]
137
- req_status &lt;name> : requests status from a node rs
148
+ req_status &lt;name&gt; : requests status from a node rs
149
+ req_neighbours &lt;name&gt; : requests for neighbours in binary form rn
150
+ trace &lt;path&gt; : run a trace, path is comma separated
138
151
  </pre>
139
152
 
140
153
  ### Interactive Mode
@@ -147,14 +160,70 @@ You'll get a prompt with the name of your node. From here you can type meshcore-
147
160
 
148
161
  The `to` command is specific to chat mode, it lets you enter the recipient for next command. By default you're on your node but you can enter other nodes or public rooms. Here are some examples :
149
162
 
150
- - `to <nodename>` : will enter nodename
163
+ - `to <dest>` : will enter dest (node or channel)
151
164
  - `to /`, `to ~` : will go to the root (your node)
152
165
  - `to ..` : will go to the last node (it will switch between the two last nodes, this is just a 1-depth history)
153
166
  - `to !` : will switch to the node you received last message from
154
167
 
155
- When you are connected to a node, the behaviour will depend on the node type, if you're on a chat node, it will send messages by default and you can chat. On a repeater or a room server, it will send commands (autocompletioin has been set to comply with the CommonCli class of meshcore). To send a message through a room you'll have to prefix the message with a quote or use the send command.
168
+ When you are in a node, the behaviour will depend on the node type, if you're on a chat node, it will send messages by default and you can chat. On a repeater or a room server, it will send commands (autocompletion has been set to comply with the CommonCli class of meshcore). To send a message through a room you'll have to prefix the message with a quote or use the send command.
156
169
 
157
- You can alse set a channel as recipient, `to public` will switch to the public channel, and `to ch1` to channel 1.
170
+ The `/` character is used to bypass the node you have currently selected using `to`:
171
+ - `/<cmd>` issues cmd command on the root
172
+ - `/<node>/<cmd>` will send cmd to selected node
173
+ - `/<dest> <msg>` will send msg to dest (channel or node)
174
+
175
+ #### Flood Scope in interactive mode
176
+
177
+ 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.
178
+
179
+ When entering chat mode, scope will be reset to `*`, meaning classic flood.
180
+
181
+ You can switch scope using the `scope` command, or postfixing the `to` command with `%<scope>`.
182
+
183
+ 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.
184
+
185
+ #### Channel echoes
186
+
187
+ It's sometimes interesting to know the path taken by a message received from a channel or which repeaters have repeated a sent message.
188
+
189
+ The app give you the information by listening `rx_log` from the device, when obtained the information is attached to the message and can be read.
190
+
191
+ In meshcore-cli I went lower-level by implementing channel echoes. When activated (with `/set channel_echoes on`), all the channel messages will be printed on the terminal along with the SNR and path taken. When sending a message, you'll have all the repeats from 0-hop repeaters as echoes, and when a message is received, you should see information about the received message, but also all the instances of the same message that might have reached you from another path.
192
+
193
+ In the example below, a msg has been sent between two repeaters, 21 and 25. 25 repeated the message and 21 the repeat and both echoes came back to the node with different SNRs.
194
+
195
+ ```
196
+ f1down/#fdl|*> 8
197
+ #fdl f1down: 8 [25] -4.75-112
198
+ #fdl f1down: 8 [2521] 1.00-109
199
+ ```
200
+
201
+ ### Issuing batch commands to contacts with apply to
202
+
203
+ `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.
204
+
205
+ Filter is constructed with comma separated fields :
206
+
207
+ - `u`, matches modification time `<` or `>` than a timestamp (can also be days hours or minutes ago if followed by `d`,`h` or `m`)
208
+ - `t`, matches the type (1: client, 2: repeater, 3: room, 4: sensor)
209
+ - `h`, matches number of hops
210
+ - `d`, direct, similar to `h>-1`
211
+ - `f`, flood, similar to `h<0` or `h=-1`
212
+
213
+ Commands should be written as if in interactive mode, if writing from the commandline don't forget to use commas to clearly delimit fields.
214
+
215
+ 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 time parameter. The sleep will be issued after the command, it helps limiting rate through repeaters ...
216
+
217
+ #### Examples
218
+
219
+ ```
220
+ # removes all clients that have not been updated in last 2 days
221
+ at u<2d,t=1 remove_contact
222
+ # gives traces to repeaters that have been updated in the last 24h and are direct
223
+ at t=2,u>1d,d cn trace
224
+ # tries to do flood login to all repeaters
225
+ at t=2 rp login
226
+ ```
158
227
 
159
228
  ## Examples
160
229
 
@@ -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=ZuBGD-H_nXLmHSP3ffat8ZtKNfieKbLmL3TQgv8B4_I,149253
4
+ meshcore_cli-1.3.1.dist-info/METADATA,sha256=k3NMG9h1qPemlm8if4A_7JD7yH6Wi8wzhABH_0ijtJo,15873
5
+ meshcore_cli-1.3.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
6
+ meshcore_cli-1.3.1.dist-info/entry_points.txt,sha256=77V29Pyth11GteDk7tneBN3MMk8JI7bTlS-BGSmxCmI,103
7
+ meshcore_cli-1.3.1.dist-info/licenses/LICENSE,sha256=F9s987VtS0AKxW7LdB2EkLMkrdeERI7ICdLJR60A9M4,1066
8
+ meshcore_cli-1.3.1.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=NbE3xX0GE3dtq0WnlBdF79iwyeGnxHerFGGoQNLeDSc,141395
4
- meshcore_cli-1.2.14.dist-info/METADATA,sha256=DDHsEo0JnY32-PX5_tr3qUYagxHJfJ9eSqYn5FJkrdw,11658
5
- meshcore_cli-1.2.14.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
6
- meshcore_cli-1.2.14.dist-info/entry_points.txt,sha256=77V29Pyth11GteDk7tneBN3MMk8JI7bTlS-BGSmxCmI,103
7
- meshcore_cli-1.2.14.dist-info/licenses/LICENSE,sha256=F9s987VtS0AKxW7LdB2EkLMkrdeERI7ICdLJR60A9M4,1066
8
- meshcore_cli-1.2.14.dist-info/RECORD,,