meshcore-cli 1.3.12__tar.gz → 1.3.16__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcore-cli
3
- Version: 1.3.12
3
+ Version: 1.3.16
4
4
  Summary: Command line interface to meshcore companion radios
5
5
  Project-URL: Homepage, https://github.com/fdlamotte/meshcore-cli
6
6
  Project-URL: Issues, https://github.com/fdlamotte/meshcore-cli/issues
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "meshcore-cli"
7
- version = "1.3.12"
7
+ version = "1.3.16"
8
8
  authors = [
9
9
  { name="Florent de Lamotte", email="florent@frizoncorrea.fr" },
10
10
  ]
@@ -32,7 +32,7 @@ import re
32
32
  from meshcore import MeshCore, EventType, logger
33
33
 
34
34
  # Version
35
- VERSION = "v1.3.12"
35
+ VERSION = "v1.3.16"
36
36
 
37
37
  # default ble address is stored in a config file
38
38
  MCCLI_CONFIG_DIR = str(Path.home()) + "/.config/meshcore/"
@@ -1028,7 +1028,7 @@ Some cmds have an help accessible with ?<cmd>. Do ?[Tab] to get a list.
1028
1028
  if '%' in dest and scope!=None :
1029
1029
  dest_scope = dest.split("%")[-1]
1030
1030
  dest = dest[:-len(dest_scope)-1]
1031
- nc = mc.get_contact_by_name(dest)
1031
+ nc = await get_contact_from_arg(mc, dest)
1032
1032
  if nc is None:
1033
1033
  if dest == "public" :
1034
1034
  nc = {"adv_name" : "public", "type" : 0, "chan_nb" : 0}
@@ -1083,7 +1083,7 @@ Some cmds have an help accessible with ?<cmd>. Do ?[Tab] to get a list.
1083
1083
  dest_scope = dest.split("%")[-1]
1084
1084
  dest = dest[:-len(dest_scope)-1]
1085
1085
  await set_scope (mc, dest_scope)
1086
- tct = mc.get_contact_by_name(dest)
1086
+ tct = await get_contact_from_arg(mc, dest)
1087
1087
  if len(args)>1 and not tct is None: # a contact, send a message
1088
1088
  if tct["type"] == 1 or tct["type"] == 3: # client or room
1089
1089
  last_ack = await msg_ack(mc, tct, line.split(" ", 1)[1])
@@ -1106,7 +1106,7 @@ Some cmds have an help accessible with ?<cmd>. Do ?[Tab] to get a list.
1106
1106
  dest_scope = contact_name.split("%")[-1]
1107
1107
  contact_name = contact_name[:-len(dest_scope)-1]
1108
1108
  await set_scope (mc, dest_scope)
1109
- tct = mc.get_contact_by_name(contact_name)
1109
+ tct = mc.get_contact_from_arg(mc, contact_name)
1110
1110
  if tct is None:
1111
1111
  print(f"{contact_name} is not a contact")
1112
1112
  else:
@@ -1346,9 +1346,7 @@ async def process_contact_chat_line(mc, contact, line):
1346
1346
  perm = int(perm_string[1:])
1347
1347
  else:
1348
1348
  perm = int(perm_string,16)
1349
- ct=mc.get_contact_by_name(name)
1350
- if ct is None:
1351
- ct=mc.get_contact_by_key_prefix(name)
1349
+ ct= await get_contact_from_arg(mc, name)
1352
1350
  if ct is None:
1353
1351
  if name == "self" or mc.self_info["public_key"].startswith(name):
1354
1352
  key = mc.self_info["public_key"]
@@ -1847,6 +1845,22 @@ async def print_disc_trace_to (mc, contact):
1847
1845
 
1848
1846
  await next_cmd(mc, ["trace", trace])
1849
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
+
1850
1864
  async def next_cmd(mc, cmds, json_output=False):
1851
1865
  """ process next command """
1852
1866
  global ARROW_HEAD, SLASH_START, SLASH_END, INVERT_SLASH
@@ -2497,8 +2511,7 @@ async def next_cmd(mc, cmds, json_output=False):
2497
2511
  dest = None
2498
2512
 
2499
2513
  if dest is None:
2500
- await mc.ensure_contacts()
2501
- dest = mc.get_contact_by_name(cmds[1])
2514
+ dest = await get_contact_from_arg(mc, cmds[1])
2502
2515
 
2503
2516
  if dest is None:
2504
2517
  if json_output :
@@ -2549,8 +2562,7 @@ async def next_cmd(mc, cmds, json_output=False):
2549
2562
  dest = None
2550
2563
 
2551
2564
  if dest is None:
2552
- await mc.ensure_contacts()
2553
- dest = mc.get_contact_by_name(cmds[1])
2565
+ dest = await get_contact_from_arg(mc, cmds[1])
2554
2566
 
2555
2567
  if dest is None:
2556
2568
  if json_output :
@@ -2627,9 +2639,9 @@ async def next_cmd(mc, cmds, json_output=False):
2627
2639
 
2628
2640
  case "login" | "l" :
2629
2641
  argnum = 2
2630
- await mc.ensure_contacts()
2631
- contact = mc.get_contact_by_name(cmds[1])
2632
- 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
2633
2645
  if json_output :
2634
2646
  print(json.dumps({"error" : "contact unknown", "name" : cmds[1]}))
2635
2647
  else:
@@ -2666,42 +2678,57 @@ async def next_cmd(mc, cmds, json_output=False):
2666
2678
 
2667
2679
  case "logout" :
2668
2680
  argnum = 1
2669
- await mc.ensure_contacts()
2670
- contact = mc.get_contact_by_name(cmds[1])
2671
- res = await mc.commands.send_logout(contact)
2672
- logger.debug(res)
2673
- if res.type == EventType.ERROR:
2674
- print(f"Error while logout: {res}")
2675
- elif json_output :
2676
- 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]}")
2677
2687
  else:
2678
- print("Logout ok")
2679
-
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
+
2680
2697
  case "contact_timeout" :
2681
2698
  argnum = 2
2682
- await mc.ensure_contacts()
2683
- contact = mc.get_contact_by_name(cmds[1])
2684
- contact["timeout"] = float(cmds[2])
2699
+ contact = await get_contact_from_arg(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])
2685
2707
 
2686
2708
  case "disc_path" | "dp" :
2687
2709
  argnum = 1
2688
- await mc.ensure_contacts()
2689
- contact = mc.get_contact_by_name(cmds[1])
2690
- res = await discover_path(mc, contact)
2691
- if res is None:
2692
- print(f"Error while discovering path")
2693
- else:
2710
+ contact = await get_contact_from_arg(mc, cmds[1])
2711
+ if contact is None:
2694
2712
  if json_output :
2695
- print(json.dumps(res, indent=4))
2713
+ print(json.dumps({"error" : "unknown contact"}))
2696
2714
  else:
2697
- if "error" in res :
2698
- 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))
2699
2723
  else:
2700
- outp = res['out_path']
2701
- outp = outp if outp != "" else "direct"
2702
- inp = res['in_path']
2703
- inp = inp if inp != "" else "direct"
2704
- 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}")
2705
2732
 
2706
2733
  case "node_discover"|"nd" :
2707
2734
  argnum = 1
@@ -2767,143 +2794,174 @@ async def next_cmd(mc, cmds, json_output=False):
2767
2794
  case "req_telemetry"|"rt" :
2768
2795
  argnum = 1
2769
2796
  await mc.ensure_contacts()
2770
- contact = mc.get_contact_by_name(cmds[1])
2771
- timeout = 0 if not "timeout" in contact else contact["timeout"]
2772
- res = await mc.commands.req_telemetry_sync(contact, timeout)
2773
- if res is None :
2797
+ contact = await get_contact_from_arg(mc, cmds[1])
2798
+ if contact is None:
2774
2799
  if json_output :
2775
- print(json.dumps({"error" : "Getting data"}))
2800
+ print(json.dumps({"error" : "unknown contact"}))
2776
2801
  else:
2777
- print("Error getting data")
2778
- else :
2779
- print(json.dumps({
2780
- "name": contact["adv_name"],
2781
- "pubkey_pre": contact["public_key"][0:16],
2782
- "lpp": res,
2783
- }, 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))
2784
2817
 
2785
2818
  case "req_status"|"rs" :
2786
2819
  argnum = 1
2787
- await mc.ensure_contacts()
2788
- contact = mc.get_contact_by_name(cmds[1])
2789
- timeout = 0 if not "timeout" in contact else contact["timeout"]
2790
- res = await mc.commands.req_status_sync(contact, timeout)
2791
- if res is None :
2820
+ contact = await get_contact_from_arg(mc, cmds[1])
2821
+ if contact is None:
2792
2822
  if json_output :
2793
- print(json.dumps({"error" : "Getting data"}))
2823
+ print(json.dumps({"error" : "unknown contact"}))
2794
2824
  else:
2795
- print("Error getting data")
2796
- else :
2797
- 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))
2798
2836
 
2799
2837
  case "req_mma" | "rm":
2800
2838
  argnum = 3
2801
2839
  await mc.ensure_contacts()
2802
- contact = mc.get_contact_by_name(cmds[1])
2803
- if cmds[2][-1] == "s":
2804
- from_secs = int(cmds[2][0:-1])
2805
- elif cmds[2][-1] == "m":
2806
- from_secs = int(cmds[2][0:-1]) * 60
2807
- elif cmds[2][-1] == "h":
2808
- from_secs = int(cmds[2][0:-1]) * 3600
2809
- else :
2810
- from_secs = int(cmds[2]) * 60 # same as tdeck
2811
- if cmds[3][-1] == "s":
2812
- to_secs = int(cmds[3][0:-1])
2813
- elif cmds[3][-1] == "m":
2814
- to_secs = int(cmds[3][0:-1]) * 60
2815
- elif cmds[3][-1] == "h":
2816
- to_secs = int(cmds[3][0:-1]) * 3600
2817
- else :
2818
- to_secs = int(cmds[3]) * 60
2819
- timeout = 0 if not "timeout" in contact else contact["timeout"]
2820
- res = await mc.commands.req_mma_sync(contact, from_secs, to_secs, timeout)
2821
- if res is None :
2840
+ contact = await get_contact_from_arg(mc, cmds[1])
2841
+ if contact is None:
2822
2842
  if json_output :
2823
- print(json.dumps({"error" : "Getting data"}))
2843
+ print(json.dumps({"error" : "unknown contact"}))
2824
2844
  else:
2825
- print("Error getting data")
2826
- else :
2827
- 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))
2828
2872
 
2829
2873
  case "req_acl" :
2830
2874
  argnum = 1
2831
- await mc.ensure_contacts()
2832
- contact = mc.get_contact_by_name(cmds[1])
2833
- timeout = 0 if not "timeout" in contact else contact["timeout"]
2834
- res = await mc.commands.req_acl_sync(contact, timeout)
2835
- if res is None :
2875
+ contact = await get_contact_from_arg(mc, cmds[1])
2876
+ if contact is None:
2836
2877
  if json_output :
2837
- print(json.dumps({"error" : "Getting data"}))
2878
+ print(json.dumps({"error" : "unknown contact"}))
2838
2879
  else:
2839
- print("Error getting data")
2840
- else :
2841
- if json_output:
2842
- print(json.dumps(res, indent=4))
2843
- else:
2844
- for e in res:
2845
- name = e['key']
2846
- ct = mc.get_contact_by_key_prefix(e['key'])
2847
- if ct is None:
2848
- if mc.self_info["public_key"].startswith(e['key']):
2849
- name = f"{'self':<20} [{e['key']}]"
2850
- else:
2851
- name = f"{ct['adv_name']:<20} [{e['key']}]"
2852
- 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}")
2853
2902
 
2854
2903
  case "req_neighbours"|"rn" :
2855
2904
  argnum = 1
2856
- await mc.ensure_contacts()
2857
- contact = mc.get_contact_by_name(cmds[1])
2858
- timeout = 0 if not "timeout" in contact else contact["timeout"]
2859
- res = await mc.commands.fetch_all_neighbours(contact, timeout=timeout)
2860
- if res is None :
2905
+ contact = await get_contact_from_arg(mc, cmds[1])
2906
+ if contact is None:
2861
2907
  if json_output :
2862
- print(json.dumps({"error" : "Getting data"}))
2863
- else:
2864
- print("Error getting data")
2865
- else :
2866
- if json_output:
2867
- print(json.dumps(res, indent=4))
2908
+ print(json.dumps({"error" : "unknown contact"}))
2868
2909
  else:
2869
- width = os.get_terminal_size().columns
2870
- print(f"Got {res['results_count']} neighbours out of {res['neighbours_count']} from {contact['adv_name']}:")
2871
- for n in res['neighbours']:
2872
- ct = mc.get_contact_by_key_prefix(n["pubkey"])
2873
- if ct and width > 60 :
2874
- name = f"[{n['pubkey'][0:8]}] {ct['adv_name']}"
2875
- name = f"{name:30}"
2876
- elif ct :
2877
- name = f"{ct['adv_name']}"
2878
- name = f"{name:20}"
2879
- else:
2880
- name = f"[{n['pubkey']}]"
2881
-
2882
- t_s = n['secs_ago']
2883
- time_ago = f"{t_s}s"
2884
- if t_s / 86400 >= 1 : # result in days
2885
- time_ago = f"{int(t_s/86400)}d ago{f' ({time_ago})' if width > 62 else ''}"
2886
- elif t_s / 3600 >= 1 : # result in days
2887
- time_ago = f"{int(t_s/3600)}h ago{f' ({time_ago})' if width > 62 else ''}"
2888
- elif t_s / 60 >= 1 : # result in min
2889
- time_ago = f"{int(t_s/60)}m ago{f' ({time_ago})' if width > 62 else ''}"
2890
-
2891
-
2892
- 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 ''}")
2893
2946
 
2894
2947
  case "req_binary" :
2895
2948
  argnum = 2
2896
- await mc.ensure_contacts()
2897
- contact = mc.get_contact_by_name(cmds[1])
2898
- timeout = 0 if not "timeout" in contact else contact["timeout"]
2899
- res = await mc.commands.req_binary(contact, bytes.fromhex(cmds[2]), timeout)
2900
- if res is None :
2949
+ contact = await get_contact_from_arg(mc, cmds[1])
2950
+ if contact is None:
2901
2951
  if json_output :
2902
- print(json.dumps({"error" : "Getting binary data"}))
2952
+ print(json.dumps({"error" : "unknown contact"}))
2903
2953
  else:
2904
- print("Error getting binary data")
2905
- else :
2906
- 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))
2907
2965
 
2908
2966
  case "contacts" | "list" | "lc":
2909
2967
  await mc.ensure_contacts(follow=True)
@@ -2968,8 +3026,7 @@ async def next_cmd(mc, cmds, json_output=False):
2968
3026
 
2969
3027
  case "path":
2970
3028
  argnum = 1
2971
- res = await mc.ensure_contacts(follow=True)
2972
- contact = mc.get_contact_by_name(cmds[1])
3029
+ contact = await get_contact_from_arg(mc, cmds[1])
2973
3030
  if contact is None:
2974
3031
  if json_output :
2975
3032
  print(json.dumps({"error" : "contact unknown", "name" : cmds[1]}))
@@ -2993,7 +3050,7 @@ async def next_cmd(mc, cmds, json_output=False):
2993
3050
  case "contact_info" | "ci":
2994
3051
  argnum = 1
2995
3052
  res = await mc.ensure_contacts(follow=True)
2996
- contact = mc.get_contact_by_name(cmds[1])
3053
+ contact = await get_contact_from_arg(mc, cmds[1])
2997
3054
  if contact is None:
2998
3055
  if json_output :
2999
3056
  print(json.dumps({"error" : "contact unknown", "name" : cmds[1]}))
@@ -3004,8 +3061,7 @@ async def next_cmd(mc, cmds, json_output=False):
3004
3061
 
3005
3062
  case "change_path" | "cp":
3006
3063
  argnum = 2
3007
- await mc.ensure_contacts()
3008
- contact = mc.get_contact_by_name(cmds[1])
3064
+ contact = await get_contact_from_arg(mc, cmds[1])
3009
3065
  if contact is None:
3010
3066
  if json_output :
3011
3067
  print(json.dumps({"error" : "contact unknown", "name" : cmds[1]}))
@@ -3027,8 +3083,7 @@ async def next_cmd(mc, cmds, json_output=False):
3027
3083
 
3028
3084
  case "change_flags" | "cf":
3029
3085
  argnum = 2
3030
- await mc.ensure_contacts()
3031
- contact = mc.get_contact_by_name(cmds[1])
3086
+ contact = await get_contact_from_arg(mc, cmds[1])
3032
3087
  if contact is None:
3033
3088
  if json_output :
3034
3089
  print(json.dumps({"error" : "contact unknown", "name" : cmds[1]}))
@@ -3044,8 +3099,7 @@ async def next_cmd(mc, cmds, json_output=False):
3044
3099
 
3045
3100
  case "reset_path" | "rp" :
3046
3101
  argnum = 1
3047
- await mc.ensure_contacts()
3048
- contact = mc.get_contact_by_name(cmds[1])
3102
+ contact = await get_contact_from_arg(mc, cmds[1])
3049
3103
  if contact is None:
3050
3104
  if json_output :
3051
3105
  print(json.dumps({"error" : "contact unknown", "name" : cmds[1]}))
@@ -3064,8 +3118,7 @@ async def next_cmd(mc, cmds, json_output=False):
3064
3118
 
3065
3119
  case "share_contact" | "sc":
3066
3120
  argnum = 1
3067
- await mc.ensure_contacts()
3068
- contact = mc.get_contact_by_name(cmds[1])
3121
+ contact = await get_contact_from_arg(mc, cmds[1])
3069
3122
  if contact is None:
3070
3123
  if json_output :
3071
3124
  print(json.dumps({"error" : "contact unknown", "name" : cmds[1]}))
@@ -3081,8 +3134,7 @@ async def next_cmd(mc, cmds, json_output=False):
3081
3134
 
3082
3135
  case "export_contact"|"ec":
3083
3136
  argnum = 1
3084
- await mc.ensure_contacts()
3085
- contact = mc.get_contact_by_name(cmds[1])
3137
+ contact = await get_contact_from_arg(mc, cmds[1])
3086
3138
  if contact is None:
3087
3139
  if json_output :
3088
3140
  print(json.dumps({"error" : "contact unknown", "name" : cmds[1]}))
@@ -3112,8 +3164,7 @@ async def next_cmd(mc, cmds, json_output=False):
3112
3164
 
3113
3165
  case "upload_contact" | "uc" :
3114
3166
  argnum = 1
3115
- await mc.ensure_contacts()
3116
- contact = mc.get_contact_by_name(cmds[1])
3167
+ contact = await get_contact_from_arg(mc, cmds[1])
3117
3168
  if contact is None:
3118
3169
  if json_output :
3119
3170
  print(json.dumps({"error" : "contact unknown", "name" : cmds[1]}))
@@ -3157,7 +3208,6 @@ async def next_cmd(mc, cmds, json_output=False):
3157
3208
 
3158
3209
  case "remove_contact" :
3159
3210
  argnum = 1
3160
- await mc.ensure_contacts()
3161
3211
  contact = mc.get_contact_by_name(cmds[1])
3162
3212
  if contact is None:
3163
3213
  if json_output :
@@ -3279,8 +3329,7 @@ async def next_cmd(mc, cmds, json_output=False):
3279
3329
 
3280
3330
  case "chat_to" | "imto" | "to" :
3281
3331
  argnum = 1
3282
- await mc.ensure_contacts()
3283
- contact = mc.get_contact_by_name(cmds[1])
3332
+ contact = await get_contact_from_arg(mc, cmds[1])
3284
3333
  await interactive_loop(mc, to=contact)
3285
3334
 
3286
3335
  case "script" :
@@ -3288,8 +3337,7 @@ async def next_cmd(mc, cmds, json_output=False):
3288
3337
  await process_script(mc, cmds[1], json_output=json_output)
3289
3338
 
3290
3339
  case _ :
3291
- await mc.ensure_contacts()
3292
- contact = mc.get_contact_by_name(cmds[0])
3340
+ contact = await get_contact_from_arg(mc, cmds[0])
3293
3341
  if contact is None:
3294
3342
  logger.error(f"Unknown command : {cmd}, {cmds} not executed ...")
3295
3343
  return None
@@ -3429,6 +3477,7 @@ def command_usage() :
3429
3477
  -b <baudrate> : specify baudrate
3430
3478
  -C : toggles classic mode for prompt
3431
3479
  -c <on/off> : disables most of color output if off
3480
+ -r : repeater mode (raw text CLI, use with -s)
3432
3481
  """)
3433
3482
 
3434
3483
  def get_help_for (cmdname, context="line") :
@@ -3588,6 +3637,230 @@ To remove a channel, use remove_channel, either with channel name or number.
3588
3637
  else:
3589
3638
  print(f"Sorry, no help yet for {cmdname}")
3590
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
+ if cmd.lower() == "clock sync" or cmd.lower() == "st" or cmd.lower() == "sync_time":
3833
+ cur_time = int(time.time())
3834
+ print(f'{ANSI_GREEN}Syncing clock to'
3835
+ f' {datetime.datetime.fromtimestamp(cur_time).strftime("%Y-%m-%d %H:%M:%S")}'
3836
+ f' ({cur_time}){ANSI_END}')
3837
+ cmd = f"time {cur_time}"
3838
+
3839
+ # Send command with CR terminator
3840
+ ser.write(f"{cmd}\r".encode())
3841
+ await asyncio.sleep(0.3)
3842
+
3843
+ # Read response
3844
+ response = ser.read(ser.in_waiting or 4096).decode(errors='ignore')
3845
+ if response:
3846
+ # Clean up echo and format response
3847
+ lines = response.strip().split('\n')
3848
+ for line in lines:
3849
+ line = line.strip()
3850
+ if line and line != cmd: # Skip echo of command
3851
+ # Color code certain responses
3852
+ if line.startswith("OK") or line.startswith("ok"):
3853
+ print(f"{ANSI_GREEN}{line}{ANSI_END}")
3854
+ elif line.startswith("Error") or line.startswith("ERR"):
3855
+ print(f"{ANSI_RED}{line}{ANSI_END}")
3856
+ elif line.startswith("->"):
3857
+ print(f"{ANSI_CYAN}{line}{ANSI_END}")
3858
+ else:
3859
+ print(line)
3860
+
3861
+ ser.close()
3862
+ print(f"\n{ANSI_BGRAY}Disconnected from repeater.{ANSI_END}")
3863
+
3591
3864
  async def main(argv):
3592
3865
  """ Do the job """
3593
3866
  json_output = JSON
@@ -3598,6 +3871,7 @@ async def main(argv):
3598
3871
  hostname = None
3599
3872
  serial_port = None
3600
3873
  baudrate = 115200
3874
+ repeater_mode = False
3601
3875
  timeout = 2
3602
3876
  pin = None
3603
3877
  first_device = False
@@ -3608,7 +3882,7 @@ async def main(argv):
3608
3882
  address = f.readline().strip()
3609
3883
 
3610
3884
  try:
3611
- opts, args = getopt.getopt(argv, "a:d:s:ht:p:b:fjDhvSlT:Pc:C")
3885
+ opts, args = getopt.getopt(argv, "a:d:s:ht:p:b:fjDhvSlT:Pc:Cr")
3612
3886
  except getopt.GetoptError:
3613
3887
  print("Unrecognized option, use -h to get more help")
3614
3888
  command_usage()
@@ -3620,6 +3894,8 @@ async def main(argv):
3620
3894
  process_event_message.color = False
3621
3895
  case "-C":
3622
3896
  interactive_loop.classic = not interactive_loop.classic
3897
+ case "-r": # repeater mode (raw text CLI)
3898
+ repeater_mode = True
3623
3899
  case "-d" : # name specified on cmdline
3624
3900
  address = arg
3625
3901
  case "-a" : # address specified on cmdline
@@ -3707,6 +3983,15 @@ async def main(argv):
3707
3983
  elif (json_output) :
3708
3984
  logger.setLevel(logging.ERROR)
3709
3985
 
3986
+ # Repeater mode - raw text CLI over serial
3987
+ if repeater_mode:
3988
+ if serial_port is None:
3989
+ print("Error: Repeater mode (-r) requires serial port (-s)")
3990
+ command_usage()
3991
+ return
3992
+ await repeater_loop(serial_port, baudrate)
3993
+ return
3994
+
3710
3995
  mc = None
3711
3996
  if not hostname is None : # connect via tcp
3712
3997
  mc = await MeshCore.create_tcp(host=hostname, port=port, debug=debug, only_error=json_output)
File without changes
File without changes
File without changes
File without changes
File without changes