meshcore-cli 1.3.10__py3-none-any.whl → 1.3.15__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.10"
35
+ VERSION = "v1.3.15"
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:
@@ -1019,7 +1028,7 @@ Some cmds have an help accessible with ?<cmd>. Do ?[Tab] to get a list.
1019
1028
  if '%' in dest and scope!=None :
1020
1029
  dest_scope = dest.split("%")[-1]
1021
1030
  dest = dest[:-len(dest_scope)-1]
1022
- nc = mc.get_contact_by_name(dest)
1031
+ nc = await get_contact_from_arg(mc, dest)
1023
1032
  if nc is None:
1024
1033
  if dest == "public" :
1025
1034
  nc = {"adv_name" : "public", "type" : 0, "chan_nb" : 0}
@@ -1074,7 +1083,7 @@ Some cmds have an help accessible with ?<cmd>. Do ?[Tab] to get a list.
1074
1083
  dest_scope = dest.split("%")[-1]
1075
1084
  dest = dest[:-len(dest_scope)-1]
1076
1085
  await set_scope (mc, dest_scope)
1077
- tct = mc.get_contact_by_name(dest)
1086
+ tct = await get_contact_from_arg(mc, dest)
1078
1087
  if len(args)>1 and not tct is None: # a contact, send a message
1079
1088
  if tct["type"] == 1 or tct["type"] == 3: # client or room
1080
1089
  last_ack = await msg_ack(mc, tct, line.split(" ", 1)[1])
@@ -1097,7 +1106,7 @@ Some cmds have an help accessible with ?<cmd>. Do ?[Tab] to get a list.
1097
1106
  dest_scope = contact_name.split("%")[-1]
1098
1107
  contact_name = contact_name[:-len(dest_scope)-1]
1099
1108
  await set_scope (mc, dest_scope)
1100
- tct = mc.get_contact_by_name(contact_name)
1109
+ tct = mc.get_contact_from_arg(mc, contact_name)
1101
1110
  if tct is None:
1102
1111
  print(f"{contact_name} is not a contact")
1103
1112
  else:
@@ -1337,9 +1346,7 @@ async def process_contact_chat_line(mc, contact, line):
1337
1346
  perm = int(perm_string[1:])
1338
1347
  else:
1339
1348
  perm = int(perm_string,16)
1340
- ct=mc.get_contact_by_name(name)
1341
- if ct is None:
1342
- ct=mc.get_contact_by_key_prefix(name)
1349
+ ct= await get_contact_from_arg(mc, name)
1343
1350
  if ct is None:
1344
1351
  if name == "self" or mc.self_info["public_key"].startswith(name):
1345
1352
  key = mc.self_info["public_key"]
@@ -1538,10 +1545,11 @@ async def send_cmd (mc, contact, cmd) :
1538
1545
  if isinstance(contact, dict):
1539
1546
  sent = res.payload.copy()
1540
1547
  sent["type"] = "SENT_CMD"
1541
- sent["name"] = contact["adv_name"]
1548
+ sent["recipient"] = contact["adv_name"]
1542
1549
  sent["text"] = cmd
1543
1550
  sent["txt_type"] = 1
1544
- sent["name"] = mc.self_info['name']
1551
+ sent["sender"] = mc.self_info['name']
1552
+ sent["name"] = sent["recipient"]
1545
1553
  await log_message(mc, sent)
1546
1554
  return res
1547
1555
 
@@ -1551,9 +1559,16 @@ async def send_chan_msg(mc, nb, msg):
1551
1559
  sent = res.payload.copy()
1552
1560
  sent["type"] = "SENT_CHAN"
1553
1561
  sent["channel_idx"] = nb
1562
+ if hasattr(mc, "channels"):
1563
+ chan_name = mc.channels[nb]["channel_name"]
1564
+ else:
1565
+ chan_name = f"channel {nb}"
1566
+ sent["chan_name"] = chan_name
1567
+ sent["recipient"] = chan_name
1554
1568
  sent["text"] = msg
1555
1569
  sent["txt_type"] = 0
1556
- sent["name"] = mc.self_info['name']
1570
+ sent["sender"] = mc.self_info['name']
1571
+ sent["name"] = chan_name
1557
1572
  await log_message(mc, sent)
1558
1573
  return res
1559
1574
 
@@ -1564,10 +1579,11 @@ async def send_msg (mc, contact, msg) :
1564
1579
  if isinstance(contact, dict):
1565
1580
  sent = res.payload.copy()
1566
1581
  sent["type"] = "SENT_MSG"
1567
- sent["name"] = contact["adv_name"]
1582
+ sent["recipient"] = contact["adv_name"]
1568
1583
  sent["text"] = msg
1569
1584
  sent["txt_type"] = 0
1570
- sent["name"] = mc.self_info['name']
1585
+ sent["sender"] = mc.self_info['name']
1586
+ sent["name"] = sent["recipient"]
1571
1587
  await log_message(mc, sent)
1572
1588
  return res
1573
1589
 
@@ -1584,10 +1600,11 @@ async def msg_ack (mc, contact, msg) :
1584
1600
  if isinstance(contact, dict):
1585
1601
  sent = res.payload.copy()
1586
1602
  sent["type"] = "SENT_MSG"
1587
- sent["name"] = contact["adv_name"]
1603
+ sent["recipient"] = contact["adv_name"]
1588
1604
  sent["text"] = msg
1589
1605
  sent["txt_type"] = 0
1590
- sent["name"] = mc.self_info['name']
1606
+ sent["sender"] = mc.self_info['name']
1607
+ sent["name"] = sent["recipient"]
1591
1608
  await log_message(mc, sent)
1592
1609
  return not res is None
1593
1610
  msg_ack.max_attempts=3
@@ -1828,6 +1845,22 @@ async def print_disc_trace_to (mc, contact):
1828
1845
 
1829
1846
  await next_cmd(mc, ["trace", trace])
1830
1847
 
1848
+
1849
+ async def get_contact_from_arg(mc, arg):
1850
+ contact = None
1851
+ await mc.ensure_contacts()
1852
+
1853
+ # first try with key prefix
1854
+ try: # try only if its a valid hex
1855
+ int(arg ,16)
1856
+ contact = mc.get_contact_by_key_prefix(arg)
1857
+ except ValueError:
1858
+ pass
1859
+ if contact is None: # try by name
1860
+ contact = mc.get_contact_by_name(arg)
1861
+
1862
+ return contact
1863
+
1831
1864
  async def next_cmd(mc, cmds, json_output=False):
1832
1865
  """ process next command """
1833
1866
  global ARROW_HEAD, SLASH_START, SLASH_END, INVERT_SLASH
@@ -2478,8 +2511,7 @@ async def next_cmd(mc, cmds, json_output=False):
2478
2511
  dest = None
2479
2512
 
2480
2513
  if dest is None:
2481
- await mc.ensure_contacts()
2482
- dest = mc.get_contact_by_name(cmds[1])
2514
+ dest = await get_contact_from_arg(mc, cmds[1])
2483
2515
 
2484
2516
  if dest is None:
2485
2517
  if json_output :
@@ -2500,7 +2532,9 @@ async def next_cmd(mc, cmds, json_output=False):
2500
2532
  if cmds[1].isnumeric() :
2501
2533
  nb = int(cmds[1])
2502
2534
  else:
2503
- nb = get_channel_by_name(mc, cmds[1])['channel_idx']
2535
+ chan = await get_channel_by_name(mc, cmds[1])
2536
+ print (chan)
2537
+ nb = chan['channel_idx']
2504
2538
  res = await send_chan_msg(mc, nb, cmds[2])
2505
2539
  logger.debug(res)
2506
2540
  if res.type == EventType.ERROR:
@@ -2528,8 +2562,7 @@ async def next_cmd(mc, cmds, json_output=False):
2528
2562
  dest = None
2529
2563
 
2530
2564
  if dest is None:
2531
- await mc.ensure_contacts()
2532
- dest = mc.get_contact_by_name(cmds[1])
2565
+ dest = await get_contact_from_arg(mc, cmds[1])
2533
2566
 
2534
2567
  if dest is None:
2535
2568
  if json_output :
@@ -2606,9 +2639,9 @@ async def next_cmd(mc, cmds, json_output=False):
2606
2639
 
2607
2640
  case "login" | "l" :
2608
2641
  argnum = 2
2609
- await mc.ensure_contacts()
2610
- contact = mc.get_contact_by_name(cmds[1])
2611
- if contact is None:
2642
+ contact = await get_contact_from_arg(mc, cmds[1])
2643
+
2644
+ if contact is None: # still none ? contact not found
2612
2645
  if json_output :
2613
2646
  print(json.dumps({"error" : "contact unknown", "name" : cmds[1]}))
2614
2647
  else:
@@ -2645,42 +2678,57 @@ async def next_cmd(mc, cmds, json_output=False):
2645
2678
 
2646
2679
  case "logout" :
2647
2680
  argnum = 1
2648
- await mc.ensure_contacts()
2649
- contact = mc.get_contact_by_name(cmds[1])
2650
- res = await mc.commands.send_logout(contact)
2651
- logger.debug(res)
2652
- if res.type == EventType.ERROR:
2653
- print(f"Error while logout: {res}")
2654
- elif json_output :
2655
- print(json.dumps(res.payload))
2681
+ contact = await get_contact_from_arg(mc, cmds[1])
2682
+ if contact is None:
2683
+ if json_output :
2684
+ print(json.dumps({"error" : "unknown contact"}))
2685
+ else:
2686
+ print(f"Unknown contact {cmds[1]}")
2656
2687
  else:
2657
- print("Logout ok")
2658
-
2688
+ res = await mc.commands.send_logout(contact)
2689
+ logger.debug(res)
2690
+ if res.type == EventType.ERROR:
2691
+ print(f"Error while logout: {res}")
2692
+ elif json_output :
2693
+ print(json.dumps(res.payload))
2694
+ else:
2695
+ print("Logout ok")
2696
+
2659
2697
  case "contact_timeout" :
2660
2698
  argnum = 2
2661
- await mc.ensure_contacts()
2662
- contact = mc.get_contact_by_name(cmds[1])
2663
- contact["timeout"] = float(cmds[2])
2699
+ contact = await get_contact_from_args(mc, cmds[1])
2700
+ if contact is None:
2701
+ if json_output :
2702
+ print(json.dumps({"error" : "unknown contact"}))
2703
+ else:
2704
+ print(f"Unknown contact {cmds[1]}")
2705
+ else:
2706
+ contact["timeout"] = float(cmds[2])
2664
2707
 
2665
2708
  case "disc_path" | "dp" :
2666
2709
  argnum = 1
2667
- await mc.ensure_contacts()
2668
- contact = mc.get_contact_by_name(cmds[1])
2669
- res = await discover_path(mc, contact)
2670
- if res is None:
2671
- print(f"Error while discovering path")
2672
- else:
2710
+ contact = await get_contact_from_arg(mc, cmds[1])
2711
+ if contact is None:
2673
2712
  if json_output :
2674
- print(json.dumps(res, indent=4))
2713
+ print(json.dumps({"error" : "unknown contact"}))
2675
2714
  else:
2676
- if "error" in res :
2677
- print("Timeout while discovering path")
2715
+ print(f"Unknown contact {cmds[1]}")
2716
+ else:
2717
+ res = await discover_path(mc, contact)
2718
+ if res is None:
2719
+ print(f"Error while discovering path")
2720
+ else:
2721
+ if json_output :
2722
+ print(json.dumps(res, indent=4))
2678
2723
  else:
2679
- outp = res['out_path']
2680
- outp = outp if outp != "" else "direct"
2681
- inp = res['in_path']
2682
- inp = inp if inp != "" else "direct"
2683
- print(f"Path for {contact['adv_name']}: out {outp}, in {inp}")
2724
+ if "error" in res :
2725
+ print("Timeout while discovering path")
2726
+ else:
2727
+ outp = res['out_path']
2728
+ outp = outp if outp != "" else "direct"
2729
+ inp = res['in_path']
2730
+ inp = inp if inp != "" else "direct"
2731
+ print(f"Path for {contact['adv_name']}: out {outp}, in {inp}")
2684
2732
 
2685
2733
  case "node_discover"|"nd" :
2686
2734
  argnum = 1
@@ -2746,143 +2794,174 @@ async def next_cmd(mc, cmds, json_output=False):
2746
2794
  case "req_telemetry"|"rt" :
2747
2795
  argnum = 1
2748
2796
  await mc.ensure_contacts()
2749
- contact = mc.get_contact_by_name(cmds[1])
2750
- timeout = 0 if not "timeout" in contact else contact["timeout"]
2751
- res = await mc.commands.req_telemetry_sync(contact, timeout)
2752
- if res is None :
2797
+ contact = await get_contact_from_arg(mc, cmds[1])
2798
+ if contact is None:
2753
2799
  if json_output :
2754
- print(json.dumps({"error" : "Getting data"}))
2800
+ print(json.dumps({"error" : "unknown contact"}))
2755
2801
  else:
2756
- print("Error getting data")
2757
- else :
2758
- print(json.dumps({
2759
- "name": contact["adv_name"],
2760
- "pubkey_pre": contact["public_key"][0:16],
2761
- "lpp": res,
2762
- }, indent = 4))
2802
+ print(f"Unknown contact {cmds[1]}")
2803
+ else:
2804
+ timeout = 0 if not "timeout" in contact else contact["timeout"]
2805
+ res = await mc.commands.req_telemetry_sync(contact, timeout)
2806
+ if res is None :
2807
+ if json_output :
2808
+ print(json.dumps({"error" : "Getting data"}))
2809
+ else:
2810
+ print("Error getting data")
2811
+ else :
2812
+ print(json.dumps({
2813
+ "name": contact["adv_name"],
2814
+ "pubkey_pre": contact["public_key"][0:16],
2815
+ "lpp": res,
2816
+ }, indent = 4))
2763
2817
 
2764
2818
  case "req_status"|"rs" :
2765
2819
  argnum = 1
2766
- await mc.ensure_contacts()
2767
- contact = mc.get_contact_by_name(cmds[1])
2768
- timeout = 0 if not "timeout" in contact else contact["timeout"]
2769
- res = await mc.commands.req_status_sync(contact, timeout)
2770
- if res is None :
2820
+ contact = await get_contact_from_arg(mc, cmds[1])
2821
+ if contact is None:
2771
2822
  if json_output :
2772
- print(json.dumps({"error" : "Getting data"}))
2823
+ print(json.dumps({"error" : "unknown contact"}))
2773
2824
  else:
2774
- print("Error getting data")
2775
- else :
2776
- print(json.dumps(res, indent=4))
2825
+ print(f"Unknown contact {cmds[1]}")
2826
+ else:
2827
+ timeout = 0 if not "timeout" in contact else contact["timeout"]
2828
+ res = await mc.commands.req_status_sync(contact, timeout)
2829
+ if res is None :
2830
+ if json_output :
2831
+ print(json.dumps({"error" : "Getting data"}))
2832
+ else:
2833
+ print("Error getting data")
2834
+ else :
2835
+ print(json.dumps(res, indent=4))
2777
2836
 
2778
2837
  case "req_mma" | "rm":
2779
2838
  argnum = 3
2780
2839
  await mc.ensure_contacts()
2781
- contact = mc.get_contact_by_name(cmds[1])
2782
- if cmds[2][-1] == "s":
2783
- from_secs = int(cmds[2][0:-1])
2784
- elif cmds[2][-1] == "m":
2785
- from_secs = int(cmds[2][0:-1]) * 60
2786
- elif cmds[2][-1] == "h":
2787
- from_secs = int(cmds[2][0:-1]) * 3600
2788
- else :
2789
- from_secs = int(cmds[2]) * 60 # same as tdeck
2790
- if cmds[3][-1] == "s":
2791
- to_secs = int(cmds[3][0:-1])
2792
- elif cmds[3][-1] == "m":
2793
- to_secs = int(cmds[3][0:-1]) * 60
2794
- elif cmds[3][-1] == "h":
2795
- to_secs = int(cmds[3][0:-1]) * 3600
2796
- else :
2797
- to_secs = int(cmds[3]) * 60
2798
- timeout = 0 if not "timeout" in contact else contact["timeout"]
2799
- res = await mc.commands.req_mma_sync(contact, from_secs, to_secs, timeout)
2800
- if res is None :
2840
+ contact = await get_contact_from_arg(mc, cmds[1])
2841
+ if contact is None:
2801
2842
  if json_output :
2802
- print(json.dumps({"error" : "Getting data"}))
2843
+ print(json.dumps({"error" : "unknown contact"}))
2803
2844
  else:
2804
- print("Error getting data")
2805
- else :
2806
- print(json.dumps(res, indent=4))
2845
+ print(f"Unknown contact {cmds[1]}")
2846
+ else:
2847
+ if cmds[2][-1] == "s":
2848
+ from_secs = int(cmds[2][0:-1])
2849
+ elif cmds[2][-1] == "m":
2850
+ from_secs = int(cmds[2][0:-1]) * 60
2851
+ elif cmds[2][-1] == "h":
2852
+ from_secs = int(cmds[2][0:-1]) * 3600
2853
+ else :
2854
+ from_secs = int(cmds[2]) * 60 # same as tdeck
2855
+ if cmds[3][-1] == "s":
2856
+ to_secs = int(cmds[3][0:-1])
2857
+ elif cmds[3][-1] == "m":
2858
+ to_secs = int(cmds[3][0:-1]) * 60
2859
+ elif cmds[3][-1] == "h":
2860
+ to_secs = int(cmds[3][0:-1]) * 3600
2861
+ else :
2862
+ to_secs = int(cmds[3]) * 60
2863
+ timeout = 0 if not "timeout" in contact else contact["timeout"]
2864
+ res = await mc.commands.req_mma_sync(contact, from_secs, to_secs, timeout)
2865
+ if res is None :
2866
+ if json_output :
2867
+ print(json.dumps({"error" : "Getting data"}))
2868
+ else:
2869
+ print("Error getting data")
2870
+ else :
2871
+ print(json.dumps(res, indent=4))
2807
2872
 
2808
2873
  case "req_acl" :
2809
2874
  argnum = 1
2810
- await mc.ensure_contacts()
2811
- contact = mc.get_contact_by_name(cmds[1])
2812
- timeout = 0 if not "timeout" in contact else contact["timeout"]
2813
- res = await mc.commands.req_acl_sync(contact, timeout)
2814
- if res is None :
2875
+ contact = await get_contact_from_arg(mc, cmds[1])
2876
+ if contact is None:
2815
2877
  if json_output :
2816
- print(json.dumps({"error" : "Getting data"}))
2817
- else:
2818
- print("Error getting data")
2819
- else :
2820
- if json_output:
2821
- print(json.dumps(res, indent=4))
2878
+ print(json.dumps({"error" : "unknown contact"}))
2822
2879
  else:
2823
- for e in res:
2824
- name = e['key']
2825
- ct = mc.get_contact_by_key_prefix(e['key'])
2826
- if ct is None:
2827
- if mc.self_info["public_key"].startswith(e['key']):
2828
- name = f"{'self':<20} [{e['key']}]"
2829
- else:
2830
- name = f"{ct['adv_name']:<20} [{e['key']}]"
2831
- print(f"{name:{' '}<35}: {e['perm']:02x}")
2880
+ print(f"Unknown contact {cmds[1]}")
2881
+ else:
2882
+ timeout = 0 if not "timeout" in contact else contact["timeout"]
2883
+ res = await mc.commands.req_acl_sync(contact, timeout)
2884
+ if res is None :
2885
+ if json_output :
2886
+ print(json.dumps({"error" : "Getting data"}))
2887
+ else:
2888
+ print("Error getting data")
2889
+ else :
2890
+ if json_output:
2891
+ print(json.dumps(res, indent=4))
2892
+ else:
2893
+ for e in res:
2894
+ name = e['key']
2895
+ ct = mc.get_contact_by_key_prefix(e['key'])
2896
+ if ct is None:
2897
+ if mc.self_info["public_key"].startswith(e['key']):
2898
+ name = f"{'self':<20} [{e['key']}]"
2899
+ else:
2900
+ name = f"{ct['adv_name']:<20} [{e['key']}]"
2901
+ print(f"{name:{' '}<35}: {e['perm']:02x}")
2832
2902
 
2833
2903
  case "req_neighbours"|"rn" :
2834
2904
  argnum = 1
2835
- await mc.ensure_contacts()
2836
- contact = mc.get_contact_by_name(cmds[1])
2837
- timeout = 0 if not "timeout" in contact else contact["timeout"]
2838
- res = await mc.commands.fetch_all_neighbours(contact, timeout=timeout)
2839
- if res is None :
2905
+ contact = await get_contact_from_arg(mc, cmds[1])
2906
+ if contact is None:
2840
2907
  if json_output :
2841
- print(json.dumps({"error" : "Getting data"}))
2908
+ print(json.dumps({"error" : "unknown contact"}))
2842
2909
  else:
2843
- print("Error getting data")
2844
- else :
2845
- if json_output:
2846
- print(json.dumps(res, indent=4))
2847
- else:
2848
- width = os.get_terminal_size().columns
2849
- print(f"Got {res['results_count']} neighbours out of {res['neighbours_count']} from {contact['adv_name']}:")
2850
- for n in res['neighbours']:
2851
- ct = mc.get_contact_by_key_prefix(n["pubkey"])
2852
- if ct and width > 60 :
2853
- name = f"[{n['pubkey'][0:8]}] {ct['adv_name']}"
2854
- name = f"{name:30}"
2855
- elif ct :
2856
- name = f"{ct['adv_name']}"
2857
- name = f"{name:20}"
2858
- else:
2859
- name = f"[{n['pubkey']}]"
2860
-
2861
- t_s = n['secs_ago']
2862
- time_ago = f"{t_s}s"
2863
- if t_s / 86400 >= 1 : # result in days
2864
- time_ago = f"{int(t_s/86400)}d ago{f' ({time_ago})' if width > 62 else ''}"
2865
- elif t_s / 3600 >= 1 : # result in days
2866
- time_ago = f"{int(t_s/3600)}h ago{f' ({time_ago})' if width > 62 else ''}"
2867
- elif t_s / 60 >= 1 : # result in min
2868
- time_ago = f"{int(t_s/60)}m ago{f' ({time_ago})' if width > 62 else ''}"
2869
-
2870
-
2871
- print(f" {name} {time_ago}, {n['snr']}dB{' SNR' if width > 66 else ''}")
2910
+ print(f"Unknown contact {cmds[1]}")
2911
+ else:
2912
+ timeout = 0 if not "timeout" in contact else contact["timeout"]
2913
+ res = await mc.commands.fetch_all_neighbours(contact, timeout=timeout)
2914
+ if res is None :
2915
+ if json_output :
2916
+ print(json.dumps({"error" : "Getting data"}))
2917
+ else:
2918
+ print("Error getting data")
2919
+ else :
2920
+ if json_output:
2921
+ print(json.dumps(res, indent=4))
2922
+ else:
2923
+ width = os.get_terminal_size().columns
2924
+ print(f"Got {res['results_count']} neighbours out of {res['neighbours_count']} from {contact['adv_name']}:")
2925
+ for n in res['neighbours']:
2926
+ ct = mc.get_contact_by_key_prefix(n["pubkey"])
2927
+ if ct and width > 60 :
2928
+ name = f"[{n['pubkey'][0:8]}] {ct['adv_name']}"
2929
+ name = f"{name:30}"
2930
+ elif ct :
2931
+ name = f"{ct['adv_name']}"
2932
+ name = f"{name:20}"
2933
+ else:
2934
+ name = f"[{n['pubkey']}]"
2935
+
2936
+ t_s = n['secs_ago']
2937
+ time_ago = f"{t_s}s"
2938
+ if t_s / 86400 >= 1 : # result in days
2939
+ time_ago = f"{int(t_s/86400)}d ago{f' ({time_ago})' if width > 62 else ''}"
2940
+ elif t_s / 3600 >= 1 : # result in days
2941
+ time_ago = f"{int(t_s/3600)}h ago{f' ({time_ago})' if width > 62 else ''}"
2942
+ elif t_s / 60 >= 1 : # result in min
2943
+ time_ago = f"{int(t_s/60)}m ago{f' ({time_ago})' if width > 62 else ''}"
2944
+
2945
+ print(f" {name} {time_ago}, {n['snr']}dB{' SNR' if width > 66 else ''}")
2872
2946
 
2873
2947
  case "req_binary" :
2874
2948
  argnum = 2
2875
- await mc.ensure_contacts()
2876
- contact = mc.get_contact_by_name(cmds[1])
2877
- timeout = 0 if not "timeout" in contact else contact["timeout"]
2878
- res = await mc.commands.req_binary(contact, bytes.fromhex(cmds[2]), timeout)
2879
- if res is None :
2949
+ contact = await get_contact_from_arg(mc, cmds[1])
2950
+ if contact is None:
2880
2951
  if json_output :
2881
- print(json.dumps({"error" : "Getting binary data"}))
2952
+ print(json.dumps({"error" : "unknown contact"}))
2882
2953
  else:
2883
- print("Error getting binary data")
2884
- else :
2885
- print(json.dumps(res))
2954
+ print(f"Unknown contact {cmds[1]}")
2955
+ else:
2956
+ timeout = 0 if not "timeout" in contact else contact["timeout"]
2957
+ res = await mc.commands.req_binary(contact, bytes.fromhex(cmds[2]), timeout)
2958
+ if res is None :
2959
+ if json_output :
2960
+ print(json.dumps({"error" : "Getting binary data"}))
2961
+ else:
2962
+ print("Error getting binary data")
2963
+ else :
2964
+ print(json.dumps(res))
2886
2965
 
2887
2966
  case "contacts" | "list" | "lc":
2888
2967
  await mc.ensure_contacts(follow=True)
@@ -2947,8 +3026,7 @@ async def next_cmd(mc, cmds, json_output=False):
2947
3026
 
2948
3027
  case "path":
2949
3028
  argnum = 1
2950
- res = await mc.ensure_contacts(follow=True)
2951
- contact = mc.get_contact_by_name(cmds[1])
3029
+ contact = await get_contact_from_arg(mc, cmds[1])
2952
3030
  if contact is None:
2953
3031
  if json_output :
2954
3032
  print(json.dumps({"error" : "contact unknown", "name" : cmds[1]}))
@@ -2972,7 +3050,7 @@ async def next_cmd(mc, cmds, json_output=False):
2972
3050
  case "contact_info" | "ci":
2973
3051
  argnum = 1
2974
3052
  res = await mc.ensure_contacts(follow=True)
2975
- contact = mc.get_contact_by_name(cmds[1])
3053
+ contact = await get_contact_from_arg(mc, cmds[1])
2976
3054
  if contact is None:
2977
3055
  if json_output :
2978
3056
  print(json.dumps({"error" : "contact unknown", "name" : cmds[1]}))
@@ -2983,8 +3061,7 @@ async def next_cmd(mc, cmds, json_output=False):
2983
3061
 
2984
3062
  case "change_path" | "cp":
2985
3063
  argnum = 2
2986
- await mc.ensure_contacts()
2987
- contact = mc.get_contact_by_name(cmds[1])
3064
+ contact = await get_contact_from_arg(mc, cmds[1])
2988
3065
  if contact is None:
2989
3066
  if json_output :
2990
3067
  print(json.dumps({"error" : "contact unknown", "name" : cmds[1]}))
@@ -3006,8 +3083,7 @@ async def next_cmd(mc, cmds, json_output=False):
3006
3083
 
3007
3084
  case "change_flags" | "cf":
3008
3085
  argnum = 2
3009
- await mc.ensure_contacts()
3010
- contact = mc.get_contact_by_name(cmds[1])
3086
+ contact = await get_contact_from_arg(mc, cmds[1])
3011
3087
  if contact is None:
3012
3088
  if json_output :
3013
3089
  print(json.dumps({"error" : "contact unknown", "name" : cmds[1]}))
@@ -3023,8 +3099,7 @@ async def next_cmd(mc, cmds, json_output=False):
3023
3099
 
3024
3100
  case "reset_path" | "rp" :
3025
3101
  argnum = 1
3026
- await mc.ensure_contacts()
3027
- contact = mc.get_contact_by_name(cmds[1])
3102
+ contact = await get_contact_from_arg(mc, cmds[1])
3028
3103
  if contact is None:
3029
3104
  if json_output :
3030
3105
  print(json.dumps({"error" : "contact unknown", "name" : cmds[1]}))
@@ -3043,8 +3118,7 @@ async def next_cmd(mc, cmds, json_output=False):
3043
3118
 
3044
3119
  case "share_contact" | "sc":
3045
3120
  argnum = 1
3046
- await mc.ensure_contacts()
3047
- contact = mc.get_contact_by_name(cmds[1])
3121
+ contact = await get_contact_from_arg(mc, cmds[1])
3048
3122
  if contact is None:
3049
3123
  if json_output :
3050
3124
  print(json.dumps({"error" : "contact unknown", "name" : cmds[1]}))
@@ -3060,8 +3134,7 @@ async def next_cmd(mc, cmds, json_output=False):
3060
3134
 
3061
3135
  case "export_contact"|"ec":
3062
3136
  argnum = 1
3063
- await mc.ensure_contacts()
3064
- contact = mc.get_contact_by_name(cmds[1])
3137
+ contact = await get_contact_from_arg(mc, cmds[1])
3065
3138
  if contact is None:
3066
3139
  if json_output :
3067
3140
  print(json.dumps({"error" : "contact unknown", "name" : cmds[1]}))
@@ -3091,8 +3164,7 @@ async def next_cmd(mc, cmds, json_output=False):
3091
3164
 
3092
3165
  case "upload_contact" | "uc" :
3093
3166
  argnum = 1
3094
- await mc.ensure_contacts()
3095
- contact = mc.get_contact_by_name(cmds[1])
3167
+ contact = await get_contact_from_arg(mc, cmds[1])
3096
3168
  if contact is None:
3097
3169
  if json_output :
3098
3170
  print(json.dumps({"error" : "contact unknown", "name" : cmds[1]}))
@@ -3136,7 +3208,6 @@ async def next_cmd(mc, cmds, json_output=False):
3136
3208
 
3137
3209
  case "remove_contact" :
3138
3210
  argnum = 1
3139
- await mc.ensure_contacts()
3140
3211
  contact = mc.get_contact_by_name(cmds[1])
3141
3212
  if contact is None:
3142
3213
  if json_output :
@@ -3258,8 +3329,7 @@ async def next_cmd(mc, cmds, json_output=False):
3258
3329
 
3259
3330
  case "chat_to" | "imto" | "to" :
3260
3331
  argnum = 1
3261
- await mc.ensure_contacts()
3262
- contact = mc.get_contact_by_name(cmds[1])
3332
+ contact = await get_contact_from_arg(mc, cmds[1])
3263
3333
  await interactive_loop(mc, to=contact)
3264
3334
 
3265
3335
  case "script" :
@@ -3267,8 +3337,7 @@ async def next_cmd(mc, cmds, json_output=False):
3267
3337
  await process_script(mc, cmds[1], json_output=json_output)
3268
3338
 
3269
3339
  case _ :
3270
- await mc.ensure_contacts()
3271
- contact = mc.get_contact_by_name(cmds[0])
3340
+ contact = await get_contact_from_arg(mc, cmds[0])
3272
3341
  if contact is None:
3273
3342
  logger.error(f"Unknown command : {cmd}, {cmds} not executed ...")
3274
3343
  return None
@@ -3408,6 +3477,7 @@ def command_usage() :
3408
3477
  -b <baudrate> : specify baudrate
3409
3478
  -C : toggles classic mode for prompt
3410
3479
  -c <on/off> : disables most of color output if off
3480
+ -r : repeater mode (raw text CLI, use with -s)
3411
3481
  """)
3412
3482
 
3413
3483
  def get_help_for (cmdname, context="line") :
@@ -3494,7 +3564,7 @@ def get_help_for (cmdname, context="line") :
3494
3564
  json_log_rx <on/off> : logs packets incoming to device as json
3495
3565
  channel_echoes <on/off> : print repeats for channel data
3496
3566
  advert_echoes <on/off> : print repeats for adverts
3497
- echo_unk_channels <on/off> : also dump unk channels (encrypted)
3567
+ echo_unk_chans <on/off> : also dump unk channels (encrypted)
3498
3568
  color <on/off> : color off should remove ANSI codes from output
3499
3569
  meshcore-cli behaviour:
3500
3570
  classic_prompt <on/off> : activates less fancier prompt
@@ -3567,6 +3637,223 @@ To remove a channel, use remove_channel, either with channel name or number.
3567
3637
  else:
3568
3638
  print(f"Sorry, no help yet for {cmdname}")
3569
3639
 
3640
+ # Repeater mode history file
3641
+ MCCLI_REPEATER_HISTORY_FILE = MCCLI_CONFIG_DIR + "repeater_history"
3642
+
3643
+ # Repeater command completion dictionary
3644
+ REPEATER_COMMANDS = {
3645
+ "ver": None,
3646
+ "board": None,
3647
+ "reboot": None,
3648
+ "advert": None,
3649
+ "clock": {"sync": None},
3650
+ "time": None,
3651
+ "neighbors": None,
3652
+ "stats-core": None,
3653
+ "stats-radio": None,
3654
+ "stats-packets": None,
3655
+ "clear": {"stats": None},
3656
+ "log": {"start": None, "stop": None, "erase": None},
3657
+ "get": {
3658
+ "name": None, "radio": None, "tx": None, "freq": None,
3659
+ "public.key": None, "prv.key": None, "repeat": None, "role": None,
3660
+ "lat": None, "lon": None, "af": None,
3661
+ "rxdelay": None, "txdelay": None, "direct.txdelay": None,
3662
+ "flood.max": None, "flood.advert.interval": None,
3663
+ "advert.interval": None, "guest.password": None,
3664
+ "allow.read.only": None, "multi.acks": None,
3665
+ "int.thresh": None, "agc.reset.interval": None,
3666
+ "bridge.enabled": None, "bridge.delay": None,
3667
+ "bridge.source": None, "bridge.baud": None,
3668
+ "bridge.channel": None, "bridge.secret": None, "bridge.type": None,
3669
+ "adc.multiplier": None, "acl": None,
3670
+ },
3671
+ "set": {
3672
+ "name": None, "radio": None, "tx": None, "freq": None,
3673
+ "prv.key": None, "repeat": {"on": None, "off": None},
3674
+ "lat": None, "lon": None, "af": None,
3675
+ "rxdelay": None, "txdelay": None, "direct.txdelay": None,
3676
+ "flood.max": None, "flood.advert.interval": None,
3677
+ "advert.interval": None, "guest.password": None,
3678
+ "allow.read.only": {"on": None, "off": None},
3679
+ "multi.acks": None, "int.thresh": None, "agc.reset.interval": None,
3680
+ "bridge.enabled": {"on": None, "off": None},
3681
+ "bridge.delay": None, "bridge.source": None,
3682
+ "bridge.baud": None, "bridge.channel": None, "bridge.secret": None,
3683
+ "adc.multiplier": None,
3684
+ },
3685
+ "password": None,
3686
+ "erase": None,
3687
+ "gps": {"on": None, "off": None, "sync": None, "setloc": None, "advert": {"none": None, "share": None, "prefs": None}},
3688
+ "sensor": {"list": None, "get": None, "set": None},
3689
+ "region": {"get": None, "put": None, "remove": None, "save": None, "load": None, "home": None, "allowf": None, "denyf": None},
3690
+ "setperm": None,
3691
+ "tempradio": None,
3692
+ "neighbor.remove": None,
3693
+ "quit": None,
3694
+ "q": None,
3695
+ "help": None,
3696
+ }
3697
+
3698
+ REPEATER_HELP = f"""
3699
+ {ANSI_BCYAN}Repeater CLI Commands:{ANSI_END}
3700
+
3701
+ {ANSI_BGREEN}Info:{ANSI_END}
3702
+ ver - Firmware version
3703
+ board - Board name
3704
+ clock - Show current time
3705
+
3706
+ {ANSI_BGREEN}Stats:{ANSI_END}
3707
+ stats-core - Core stats (uptime, battery, queue)
3708
+ stats-radio - Radio stats (RSSI, SNR, noise floor)
3709
+ stats-packets - Packet statistics (sent/recv counts)
3710
+ clear stats - Reset all statistics
3711
+
3712
+ {ANSI_BGREEN}Network:{ANSI_END}
3713
+ neighbors - Show neighboring repeaters (zero-hop)
3714
+ advert - Send advertisement now
3715
+
3716
+ {ANSI_BGREEN}Logging:{ANSI_END}
3717
+ log start - Enable packet logging
3718
+ log stop - Disable packet logging
3719
+ log - Dump log file to console
3720
+ log erase - Erase log file
3721
+
3722
+ {ANSI_BGREEN}Configuration (get/set):{ANSI_END}
3723
+ get name - Node name
3724
+ get radio - Radio params (freq,bw,sf,cr)
3725
+ get tx - TX power (dBm)
3726
+ get repeat - Repeat mode on/off
3727
+ get public.key - Node public key
3728
+ get advert.interval - Advertisement interval (minutes)
3729
+
3730
+ set name <name> - Set node name
3731
+ set tx <power> - Set TX power (dBm)
3732
+ set repeat on|off - Enable/disable repeating
3733
+ set radio f,bw,sf,cr - Set radio params (reboot to apply)
3734
+ set advert.interval <min> - Set advert interval (60-240 min)
3735
+
3736
+ {ANSI_BGREEN}System:{ANSI_END}
3737
+ reboot - Reboot device
3738
+ erase - Erase filesystem (serial only)
3739
+
3740
+ {ANSI_BYELLOW}Type 'quit' or 'q' to exit, Ctrl+C to abort{ANSI_END}
3741
+ """
3742
+
3743
+ async def repeater_loop(port, baudrate):
3744
+ """Interactive loop for repeater text CLI (raw serial commands)"""
3745
+ import serial as pyserial
3746
+
3747
+ print(f"{ANSI_BCYAN}Connecting to repeater at {port} ({baudrate} baud)...{ANSI_END}")
3748
+ try:
3749
+ ser = pyserial.Serial(port, baudrate, timeout=1)
3750
+ except PermissionError:
3751
+ print(f"{ANSI_BRED}Error: Permission denied. Try running with sudo or add user to dialout group.{ANSI_END}")
3752
+ return
3753
+ except Exception as e:
3754
+ print(f"{ANSI_BRED}Error opening serial port: {e}{ANSI_END}")
3755
+ return
3756
+
3757
+ await asyncio.sleep(0.5) # Wait for connection to stabilize
3758
+ ser.reset_input_buffer()
3759
+
3760
+ # Send initial CR to wake up CLI
3761
+ ser.write(b"\r")
3762
+ await asyncio.sleep(0.2)
3763
+ ser.reset_input_buffer()
3764
+
3765
+ # Try to get device info
3766
+ ser.write(b"ver\r")
3767
+ await asyncio.sleep(0.3)
3768
+ ver_response = ser.read(ser.in_waiting or 256).decode(errors='ignore').strip()
3769
+ device_name = "Repeater"
3770
+ for line in ver_response.split('\n'):
3771
+ line = line.strip()
3772
+ if line and not line.startswith("ver") and ">" not in line[:3]:
3773
+ device_name = line.split('(')[0].strip() if '(' in line else line
3774
+ break
3775
+
3776
+ print(f"{ANSI_BGREEN}Connected!{ANSI_END} Device: {ANSI_BMAGENTA}{device_name}{ANSI_END}")
3777
+ print(f"Type {ANSI_BCYAN}help{ANSI_END} for commands, {ANSI_BCYAN}quit{ANSI_END} to exit, {ANSI_BCYAN}Tab{ANSI_END} for completion")
3778
+ print("-" * 50)
3779
+
3780
+ # Setup history and session
3781
+ try:
3782
+ if os.path.isdir(MCCLI_CONFIG_DIR):
3783
+ our_history = FileHistory(MCCLI_REPEATER_HISTORY_FILE)
3784
+ else:
3785
+ our_history = None
3786
+ except Exception:
3787
+ our_history = None
3788
+
3789
+ session = PromptSession(
3790
+ history=our_history,
3791
+ wrap_lines=False,
3792
+ mouse_support=False,
3793
+ complete_style=CompleteStyle.MULTI_COLUMN
3794
+ )
3795
+
3796
+ # Setup key bindings
3797
+ bindings = KeyBindings()
3798
+
3799
+ @bindings.add("escape")
3800
+ def _(event):
3801
+ event.app.current_buffer.cancel_completion()
3802
+
3803
+ # Build prompt
3804
+ prompt_base = f"{ANSI_BGRAY}{device_name}{ANSI_MAGENTA}>{ANSI_END} "
3805
+
3806
+ # Setup completer
3807
+ completer = NestedCompleter.from_nested_dict(REPEATER_COMMANDS)
3808
+
3809
+ while True:
3810
+ try:
3811
+ cmd = await session.prompt_async(
3812
+ ANSI(prompt_base),
3813
+ completer=completer,
3814
+ complete_while_typing=False,
3815
+ key_bindings=bindings
3816
+ )
3817
+ except (KeyboardInterrupt, EOFError):
3818
+ break
3819
+
3820
+ cmd = cmd.strip()
3821
+
3822
+ if not cmd:
3823
+ continue
3824
+
3825
+ if cmd.lower() in ("quit", "exit", "q"):
3826
+ break
3827
+
3828
+ if cmd.lower() == "help":
3829
+ print(REPEATER_HELP)
3830
+ continue
3831
+
3832
+ # Send command with CR terminator
3833
+ ser.write(f"{cmd}\r".encode())
3834
+ await asyncio.sleep(0.3)
3835
+
3836
+ # Read response
3837
+ response = ser.read(ser.in_waiting or 4096).decode(errors='ignore')
3838
+ if response:
3839
+ # Clean up echo and format response
3840
+ lines = response.strip().split('\n')
3841
+ for line in lines:
3842
+ line = line.strip()
3843
+ if line and line != cmd: # Skip echo of command
3844
+ # Color code certain responses
3845
+ if line.startswith("OK") or line.startswith("ok"):
3846
+ print(f"{ANSI_GREEN}{line}{ANSI_END}")
3847
+ elif line.startswith("Error") or line.startswith("ERR"):
3848
+ print(f"{ANSI_RED}{line}{ANSI_END}")
3849
+ elif line.startswith("->"):
3850
+ print(f"{ANSI_CYAN}{line}{ANSI_END}")
3851
+ else:
3852
+ print(line)
3853
+
3854
+ ser.close()
3855
+ print(f"\n{ANSI_BGRAY}Disconnected from repeater.{ANSI_END}")
3856
+
3570
3857
  async def main(argv):
3571
3858
  """ Do the job """
3572
3859
  json_output = JSON
@@ -3577,6 +3864,7 @@ async def main(argv):
3577
3864
  hostname = None
3578
3865
  serial_port = None
3579
3866
  baudrate = 115200
3867
+ repeater_mode = False
3580
3868
  timeout = 2
3581
3869
  pin = None
3582
3870
  first_device = False
@@ -3587,7 +3875,7 @@ async def main(argv):
3587
3875
  address = f.readline().strip()
3588
3876
 
3589
3877
  try:
3590
- opts, args = getopt.getopt(argv, "a:d:s:ht:p:b:fjDhvSlT:Pc:C")
3878
+ opts, args = getopt.getopt(argv, "a:d:s:ht:p:b:fjDhvSlT:Pc:Cr")
3591
3879
  except getopt.GetoptError:
3592
3880
  print("Unrecognized option, use -h to get more help")
3593
3881
  command_usage()
@@ -3599,6 +3887,8 @@ async def main(argv):
3599
3887
  process_event_message.color = False
3600
3888
  case "-C":
3601
3889
  interactive_loop.classic = not interactive_loop.classic
3890
+ case "-r": # repeater mode (raw text CLI)
3891
+ repeater_mode = True
3602
3892
  case "-d" : # name specified on cmdline
3603
3893
  address = arg
3604
3894
  case "-a" : # address specified on cmdline
@@ -3686,6 +3976,15 @@ async def main(argv):
3686
3976
  elif (json_output) :
3687
3977
  logger.setLevel(logging.ERROR)
3688
3978
 
3979
+ # Repeater mode - raw text CLI over serial
3980
+ if repeater_mode:
3981
+ if serial_port is None:
3982
+ print("Error: Repeater mode (-r) requires serial port (-s)")
3983
+ command_usage()
3984
+ return
3985
+ await repeater_loop(serial_port, baudrate)
3986
+ return
3987
+
3689
3988
  mc = None
3690
3989
  if not hostname is None : # connect via tcp
3691
3990
  mc = await MeshCore.create_tcp(host=hostname, port=port, debug=debug, only_error=json_output)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcore-cli
3
- Version: 1.3.10
3
+ Version: 1.3.15
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
@@ -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=VgOnJFXkFt8Pt3Inx7mfcqWXM5YvRBlTQialBc_RSBY,171639
4
+ meshcore_cli-1.3.15.dist-info/METADATA,sha256=Pvi791fQQ54tLnATv4LfluErw5y6ximHcH71cKZzeqE,18194
5
+ meshcore_cli-1.3.15.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
6
+ meshcore_cli-1.3.15.dist-info/entry_points.txt,sha256=77V29Pyth11GteDk7tneBN3MMk8JI7bTlS-BGSmxCmI,103
7
+ meshcore_cli-1.3.15.dist-info/licenses/LICENSE,sha256=F9s987VtS0AKxW7LdB2EkLMkrdeERI7ICdLJR60A9M4,1066
8
+ meshcore_cli-1.3.15.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=ej2OdXAoi3wqN8CJpxV02DlKURoxRVOIcN-3Io2NJdo,160033
4
- meshcore_cli-1.3.10.dist-info/METADATA,sha256=J4F-v883nXG9-t9AYD7_MevS0ONjKbrBjpAewSlGFj0,17138
5
- meshcore_cli-1.3.10.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
6
- meshcore_cli-1.3.10.dist-info/entry_points.txt,sha256=77V29Pyth11GteDk7tneBN3MMk8JI7bTlS-BGSmxCmI,103
7
- meshcore_cli-1.3.10.dist-info/licenses/LICENSE,sha256=F9s987VtS0AKxW7LdB2EkLMkrdeERI7ICdLJR60A9M4,1066
8
- meshcore_cli-1.3.10.dist-info/RECORD,,