meshcore-cli 1.3.12__tar.gz → 1.3.13__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.13
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.13"
8
8
  authors = [
9
9
  { name="Florent de Lamotte", email="florent@frizoncorrea.fr" },
10
10
  ]
@@ -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 = 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,8 +2678,7 @@ 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])
2681
+ contact = await get_contact_from_arg(mc, cmds[1])
2671
2682
  res = await mc.commands.send_logout(contact)
2672
2683
  logger.debug(res)
2673
2684
  if res.type == EventType.ERROR:
@@ -2679,14 +2690,12 @@ async def next_cmd(mc, cmds, json_output=False):
2679
2690
 
2680
2691
  case "contact_timeout" :
2681
2692
  argnum = 2
2682
- await mc.ensure_contacts()
2683
- contact = mc.get_contact_by_name(cmds[1])
2693
+ contact = await get_contact_from_args(mc, cmds[1])
2684
2694
  contact["timeout"] = float(cmds[2])
2685
2695
 
2686
2696
  case "disc_path" | "dp" :
2687
2697
  argnum = 1
2688
- await mc.ensure_contacts()
2689
- contact = mc.get_contact_by_name(cmds[1])
2698
+ contact = await get_contact_from_arg(mc, cmds[1])
2690
2699
  res = await discover_path(mc, contact)
2691
2700
  if res is None:
2692
2701
  print(f"Error while discovering path")
@@ -2767,7 +2776,7 @@ async def next_cmd(mc, cmds, json_output=False):
2767
2776
  case "req_telemetry"|"rt" :
2768
2777
  argnum = 1
2769
2778
  await mc.ensure_contacts()
2770
- contact = mc.get_contact_by_name(cmds[1])
2779
+ contact = await get_contact_from_arg(mc, cmds[1])
2771
2780
  timeout = 0 if not "timeout" in contact else contact["timeout"]
2772
2781
  res = await mc.commands.req_telemetry_sync(contact, timeout)
2773
2782
  if res is None :
@@ -2784,8 +2793,7 @@ async def next_cmd(mc, cmds, json_output=False):
2784
2793
 
2785
2794
  case "req_status"|"rs" :
2786
2795
  argnum = 1
2787
- await mc.ensure_contacts()
2788
- contact = mc.get_contact_by_name(cmds[1])
2796
+ contact = await get_contact_from_arg(mc, cmds[1])
2789
2797
  timeout = 0 if not "timeout" in contact else contact["timeout"]
2790
2798
  res = await mc.commands.req_status_sync(contact, timeout)
2791
2799
  if res is None :
@@ -2799,7 +2807,7 @@ async def next_cmd(mc, cmds, json_output=False):
2799
2807
  case "req_mma" | "rm":
2800
2808
  argnum = 3
2801
2809
  await mc.ensure_contacts()
2802
- contact = mc.get_contact_by_name(cmds[1])
2810
+ contact = await get_contact_from_arg(mc, cmds[1])
2803
2811
  if cmds[2][-1] == "s":
2804
2812
  from_secs = int(cmds[2][0:-1])
2805
2813
  elif cmds[2][-1] == "m":
@@ -2828,8 +2836,7 @@ async def next_cmd(mc, cmds, json_output=False):
2828
2836
 
2829
2837
  case "req_acl" :
2830
2838
  argnum = 1
2831
- await mc.ensure_contacts()
2832
- contact = mc.get_contact_by_name(cmds[1])
2839
+ contact = await get_contact_from_arg(mc, cmds[1])
2833
2840
  timeout = 0 if not "timeout" in contact else contact["timeout"]
2834
2841
  res = await mc.commands.req_acl_sync(contact, timeout)
2835
2842
  if res is None :
@@ -2853,8 +2860,7 @@ async def next_cmd(mc, cmds, json_output=False):
2853
2860
 
2854
2861
  case "req_neighbours"|"rn" :
2855
2862
  argnum = 1
2856
- await mc.ensure_contacts()
2857
- contact = mc.get_contact_by_name(cmds[1])
2863
+ contact = await get_contact_from_arg(mc, cmds[1])
2858
2864
  timeout = 0 if not "timeout" in contact else contact["timeout"]
2859
2865
  res = await mc.commands.fetch_all_neighbours(contact, timeout=timeout)
2860
2866
  if res is None :
@@ -2893,8 +2899,7 @@ async def next_cmd(mc, cmds, json_output=False):
2893
2899
 
2894
2900
  case "req_binary" :
2895
2901
  argnum = 2
2896
- await mc.ensure_contacts()
2897
- contact = mc.get_contact_by_name(cmds[1])
2902
+ contact = await get_contact_from_arg(mc, cmds[1])
2898
2903
  timeout = 0 if not "timeout" in contact else contact["timeout"]
2899
2904
  res = await mc.commands.req_binary(contact, bytes.fromhex(cmds[2]), timeout)
2900
2905
  if res is None :
@@ -2968,8 +2973,7 @@ async def next_cmd(mc, cmds, json_output=False):
2968
2973
 
2969
2974
  case "path":
2970
2975
  argnum = 1
2971
- res = await mc.ensure_contacts(follow=True)
2972
- contact = mc.get_contact_by_name(cmds[1])
2976
+ contact = await get_contact_from_arg(mc, cmds[1])
2973
2977
  if contact is None:
2974
2978
  if json_output :
2975
2979
  print(json.dumps({"error" : "contact unknown", "name" : cmds[1]}))
@@ -2993,7 +2997,7 @@ async def next_cmd(mc, cmds, json_output=False):
2993
2997
  case "contact_info" | "ci":
2994
2998
  argnum = 1
2995
2999
  res = await mc.ensure_contacts(follow=True)
2996
- contact = mc.get_contact_by_name(cmds[1])
3000
+ contact = await get_contact_from_arg(mc, cmds[1])
2997
3001
  if contact is None:
2998
3002
  if json_output :
2999
3003
  print(json.dumps({"error" : "contact unknown", "name" : cmds[1]}))
@@ -3004,8 +3008,7 @@ async def next_cmd(mc, cmds, json_output=False):
3004
3008
 
3005
3009
  case "change_path" | "cp":
3006
3010
  argnum = 2
3007
- await mc.ensure_contacts()
3008
- contact = mc.get_contact_by_name(cmds[1])
3011
+ contact = await get_contact_from_arg(mc, cmds[1])
3009
3012
  if contact is None:
3010
3013
  if json_output :
3011
3014
  print(json.dumps({"error" : "contact unknown", "name" : cmds[1]}))
@@ -3027,8 +3030,7 @@ async def next_cmd(mc, cmds, json_output=False):
3027
3030
 
3028
3031
  case "change_flags" | "cf":
3029
3032
  argnum = 2
3030
- await mc.ensure_contacts()
3031
- contact = mc.get_contact_by_name(cmds[1])
3033
+ contact = await get_contact_from_arg(mc, cmds[1])
3032
3034
  if contact is None:
3033
3035
  if json_output :
3034
3036
  print(json.dumps({"error" : "contact unknown", "name" : cmds[1]}))
@@ -3044,8 +3046,7 @@ async def next_cmd(mc, cmds, json_output=False):
3044
3046
 
3045
3047
  case "reset_path" | "rp" :
3046
3048
  argnum = 1
3047
- await mc.ensure_contacts()
3048
- contact = mc.get_contact_by_name(cmds[1])
3049
+ contact = await get_contact_from_arg(mc, cmds[1])
3049
3050
  if contact is None:
3050
3051
  if json_output :
3051
3052
  print(json.dumps({"error" : "contact unknown", "name" : cmds[1]}))
@@ -3064,8 +3065,7 @@ async def next_cmd(mc, cmds, json_output=False):
3064
3065
 
3065
3066
  case "share_contact" | "sc":
3066
3067
  argnum = 1
3067
- await mc.ensure_contacts()
3068
- contact = mc.get_contact_by_name(cmds[1])
3068
+ contact = await get_contact_from_arg(mc, cmds[1])
3069
3069
  if contact is None:
3070
3070
  if json_output :
3071
3071
  print(json.dumps({"error" : "contact unknown", "name" : cmds[1]}))
@@ -3081,8 +3081,7 @@ async def next_cmd(mc, cmds, json_output=False):
3081
3081
 
3082
3082
  case "export_contact"|"ec":
3083
3083
  argnum = 1
3084
- await mc.ensure_contacts()
3085
- contact = mc.get_contact_by_name(cmds[1])
3084
+ contact = await get_contact_from_arg(mc, cmds[1])
3086
3085
  if contact is None:
3087
3086
  if json_output :
3088
3087
  print(json.dumps({"error" : "contact unknown", "name" : cmds[1]}))
@@ -3112,8 +3111,7 @@ async def next_cmd(mc, cmds, json_output=False):
3112
3111
 
3113
3112
  case "upload_contact" | "uc" :
3114
3113
  argnum = 1
3115
- await mc.ensure_contacts()
3116
- contact = mc.get_contact_by_name(cmds[1])
3114
+ contact = await get_contact_from_arg(mc, cmds[1])
3117
3115
  if contact is None:
3118
3116
  if json_output :
3119
3117
  print(json.dumps({"error" : "contact unknown", "name" : cmds[1]}))
@@ -3157,7 +3155,6 @@ async def next_cmd(mc, cmds, json_output=False):
3157
3155
 
3158
3156
  case "remove_contact" :
3159
3157
  argnum = 1
3160
- await mc.ensure_contacts()
3161
3158
  contact = mc.get_contact_by_name(cmds[1])
3162
3159
  if contact is None:
3163
3160
  if json_output :
@@ -3279,8 +3276,7 @@ async def next_cmd(mc, cmds, json_output=False):
3279
3276
 
3280
3277
  case "chat_to" | "imto" | "to" :
3281
3278
  argnum = 1
3282
- await mc.ensure_contacts()
3283
- contact = mc.get_contact_by_name(cmds[1])
3279
+ contact = await get_contact_from_arg(mc, cmds[1])
3284
3280
  await interactive_loop(mc, to=contact)
3285
3281
 
3286
3282
  case "script" :
@@ -3288,8 +3284,7 @@ async def next_cmd(mc, cmds, json_output=False):
3288
3284
  await process_script(mc, cmds[1], json_output=json_output)
3289
3285
 
3290
3286
  case _ :
3291
- await mc.ensure_contacts()
3292
- contact = mc.get_contact_by_name(cmds[0])
3287
+ contact = await get_contact_from_arg(mc, cmds[0])
3293
3288
  if contact is None:
3294
3289
  logger.error(f"Unknown command : {cmd}, {cmds} not executed ...")
3295
3290
  return None
@@ -3429,6 +3424,7 @@ def command_usage() :
3429
3424
  -b <baudrate> : specify baudrate
3430
3425
  -C : toggles classic mode for prompt
3431
3426
  -c <on/off> : disables most of color output if off
3427
+ -r : repeater mode (raw text CLI, use with -s)
3432
3428
  """)
3433
3429
 
3434
3430
  def get_help_for (cmdname, context="line") :
@@ -3588,6 +3584,223 @@ To remove a channel, use remove_channel, either with channel name or number.
3588
3584
  else:
3589
3585
  print(f"Sorry, no help yet for {cmdname}")
3590
3586
 
3587
+ # Repeater mode history file
3588
+ MCCLI_REPEATER_HISTORY_FILE = MCCLI_CONFIG_DIR + "repeater_history"
3589
+
3590
+ # Repeater command completion dictionary
3591
+ REPEATER_COMMANDS = {
3592
+ "ver": None,
3593
+ "board": None,
3594
+ "reboot": None,
3595
+ "advert": None,
3596
+ "clock": {"sync": None},
3597
+ "time": None,
3598
+ "neighbors": None,
3599
+ "stats-core": None,
3600
+ "stats-radio": None,
3601
+ "stats-packets": None,
3602
+ "clear": {"stats": None},
3603
+ "log": {"start": None, "stop": None, "erase": None},
3604
+ "get": {
3605
+ "name": None, "radio": None, "tx": None, "freq": None,
3606
+ "public.key": None, "prv.key": None, "repeat": None, "role": None,
3607
+ "lat": None, "lon": None, "af": None,
3608
+ "rxdelay": None, "txdelay": None, "direct.txdelay": None,
3609
+ "flood.max": None, "flood.advert.interval": None,
3610
+ "advert.interval": None, "guest.password": None,
3611
+ "allow.read.only": None, "multi.acks": None,
3612
+ "int.thresh": None, "agc.reset.interval": None,
3613
+ "bridge.enabled": None, "bridge.delay": None,
3614
+ "bridge.source": None, "bridge.baud": None,
3615
+ "bridge.channel": None, "bridge.secret": None, "bridge.type": None,
3616
+ "adc.multiplier": None, "acl": None,
3617
+ },
3618
+ "set": {
3619
+ "name": None, "radio": None, "tx": None, "freq": None,
3620
+ "prv.key": None, "repeat": {"on": None, "off": None},
3621
+ "lat": None, "lon": None, "af": None,
3622
+ "rxdelay": None, "txdelay": None, "direct.txdelay": None,
3623
+ "flood.max": None, "flood.advert.interval": None,
3624
+ "advert.interval": None, "guest.password": None,
3625
+ "allow.read.only": {"on": None, "off": None},
3626
+ "multi.acks": None, "int.thresh": None, "agc.reset.interval": None,
3627
+ "bridge.enabled": {"on": None, "off": None},
3628
+ "bridge.delay": None, "bridge.source": None,
3629
+ "bridge.baud": None, "bridge.channel": None, "bridge.secret": None,
3630
+ "adc.multiplier": None,
3631
+ },
3632
+ "password": None,
3633
+ "erase": None,
3634
+ "gps": {"on": None, "off": None, "sync": None, "setloc": None, "advert": {"none": None, "share": None, "prefs": None}},
3635
+ "sensor": {"list": None, "get": None, "set": None},
3636
+ "region": {"get": None, "put": None, "remove": None, "save": None, "load": None, "home": None, "allowf": None, "denyf": None},
3637
+ "setperm": None,
3638
+ "tempradio": None,
3639
+ "neighbor.remove": None,
3640
+ "quit": None,
3641
+ "q": None,
3642
+ "help": None,
3643
+ }
3644
+
3645
+ REPEATER_HELP = f"""
3646
+ {ANSI_BCYAN}Repeater CLI Commands:{ANSI_END}
3647
+
3648
+ {ANSI_BGREEN}Info:{ANSI_END}
3649
+ ver - Firmware version
3650
+ board - Board name
3651
+ clock - Show current time
3652
+
3653
+ {ANSI_BGREEN}Stats:{ANSI_END}
3654
+ stats-core - Core stats (uptime, battery, queue)
3655
+ stats-radio - Radio stats (RSSI, SNR, noise floor)
3656
+ stats-packets - Packet statistics (sent/recv counts)
3657
+ clear stats - Reset all statistics
3658
+
3659
+ {ANSI_BGREEN}Network:{ANSI_END}
3660
+ neighbors - Show neighboring repeaters (zero-hop)
3661
+ advert - Send advertisement now
3662
+
3663
+ {ANSI_BGREEN}Logging:{ANSI_END}
3664
+ log start - Enable packet logging
3665
+ log stop - Disable packet logging
3666
+ log - Dump log file to console
3667
+ log erase - Erase log file
3668
+
3669
+ {ANSI_BGREEN}Configuration (get/set):{ANSI_END}
3670
+ get name - Node name
3671
+ get radio - Radio params (freq,bw,sf,cr)
3672
+ get tx - TX power (dBm)
3673
+ get repeat - Repeat mode on/off
3674
+ get public.key - Node public key
3675
+ get advert.interval - Advertisement interval (minutes)
3676
+
3677
+ set name <name> - Set node name
3678
+ set tx <power> - Set TX power (dBm)
3679
+ set repeat on|off - Enable/disable repeating
3680
+ set radio f,bw,sf,cr - Set radio params (reboot to apply)
3681
+ set advert.interval <min> - Set advert interval (60-240 min)
3682
+
3683
+ {ANSI_BGREEN}System:{ANSI_END}
3684
+ reboot - Reboot device
3685
+ erase - Erase filesystem (serial only)
3686
+
3687
+ {ANSI_BYELLOW}Type 'quit' or 'q' to exit, Ctrl+C to abort{ANSI_END}
3688
+ """
3689
+
3690
+ async def repeater_loop(port, baudrate):
3691
+ """Interactive loop for repeater text CLI (raw serial commands)"""
3692
+ import serial as pyserial
3693
+
3694
+ print(f"{ANSI_BCYAN}Connecting to repeater at {port} ({baudrate} baud)...{ANSI_END}")
3695
+ try:
3696
+ ser = pyserial.Serial(port, baudrate, timeout=1)
3697
+ except PermissionError:
3698
+ print(f"{ANSI_BRED}Error: Permission denied. Try running with sudo or add user to dialout group.{ANSI_END}")
3699
+ return
3700
+ except Exception as e:
3701
+ print(f"{ANSI_BRED}Error opening serial port: {e}{ANSI_END}")
3702
+ return
3703
+
3704
+ await asyncio.sleep(0.5) # Wait for connection to stabilize
3705
+ ser.reset_input_buffer()
3706
+
3707
+ # Send initial CR to wake up CLI
3708
+ ser.write(b"\r")
3709
+ await asyncio.sleep(0.2)
3710
+ ser.reset_input_buffer()
3711
+
3712
+ # Try to get device info
3713
+ ser.write(b"ver\r")
3714
+ await asyncio.sleep(0.3)
3715
+ ver_response = ser.read(ser.in_waiting or 256).decode(errors='ignore').strip()
3716
+ device_name = "Repeater"
3717
+ for line in ver_response.split('\n'):
3718
+ line = line.strip()
3719
+ if line and not line.startswith("ver") and ">" not in line[:3]:
3720
+ device_name = line.split('(')[0].strip() if '(' in line else line
3721
+ break
3722
+
3723
+ print(f"{ANSI_BGREEN}Connected!{ANSI_END} Device: {ANSI_BMAGENTA}{device_name}{ANSI_END}")
3724
+ print(f"Type {ANSI_BCYAN}help{ANSI_END} for commands, {ANSI_BCYAN}quit{ANSI_END} to exit, {ANSI_BCYAN}Tab{ANSI_END} for completion")
3725
+ print("-" * 50)
3726
+
3727
+ # Setup history and session
3728
+ try:
3729
+ if os.path.isdir(MCCLI_CONFIG_DIR):
3730
+ our_history = FileHistory(MCCLI_REPEATER_HISTORY_FILE)
3731
+ else:
3732
+ our_history = None
3733
+ except Exception:
3734
+ our_history = None
3735
+
3736
+ session = PromptSession(
3737
+ history=our_history,
3738
+ wrap_lines=False,
3739
+ mouse_support=False,
3740
+ complete_style=CompleteStyle.MULTI_COLUMN
3741
+ )
3742
+
3743
+ # Setup key bindings
3744
+ bindings = KeyBindings()
3745
+
3746
+ @bindings.add("escape")
3747
+ def _(event):
3748
+ event.app.current_buffer.cancel_completion()
3749
+
3750
+ # Build prompt
3751
+ prompt_base = f"{ANSI_BGRAY}{device_name}{ANSI_MAGENTA}>{ANSI_END} "
3752
+
3753
+ # Setup completer
3754
+ completer = NestedCompleter.from_nested_dict(REPEATER_COMMANDS)
3755
+
3756
+ while True:
3757
+ try:
3758
+ cmd = await session.prompt_async(
3759
+ ANSI(prompt_base),
3760
+ completer=completer,
3761
+ complete_while_typing=False,
3762
+ key_bindings=bindings
3763
+ )
3764
+ except (KeyboardInterrupt, EOFError):
3765
+ break
3766
+
3767
+ cmd = cmd.strip()
3768
+
3769
+ if not cmd:
3770
+ continue
3771
+
3772
+ if cmd.lower() in ("quit", "exit", "q"):
3773
+ break
3774
+
3775
+ if cmd.lower() == "help":
3776
+ print(REPEATER_HELP)
3777
+ continue
3778
+
3779
+ # Send command with CR terminator
3780
+ ser.write(f"{cmd}\r".encode())
3781
+ await asyncio.sleep(0.3)
3782
+
3783
+ # Read response
3784
+ response = ser.read(ser.in_waiting or 4096).decode(errors='ignore')
3785
+ if response:
3786
+ # Clean up echo and format response
3787
+ lines = response.strip().split('\n')
3788
+ for line in lines:
3789
+ line = line.strip()
3790
+ if line and line != cmd: # Skip echo of command
3791
+ # Color code certain responses
3792
+ if line.startswith("OK") or line.startswith("ok"):
3793
+ print(f"{ANSI_GREEN}{line}{ANSI_END}")
3794
+ elif line.startswith("Error") or line.startswith("ERR"):
3795
+ print(f"{ANSI_RED}{line}{ANSI_END}")
3796
+ elif line.startswith("->"):
3797
+ print(f"{ANSI_CYAN}{line}{ANSI_END}")
3798
+ else:
3799
+ print(line)
3800
+
3801
+ ser.close()
3802
+ print(f"\n{ANSI_BGRAY}Disconnected from repeater.{ANSI_END}")
3803
+
3591
3804
  async def main(argv):
3592
3805
  """ Do the job """
3593
3806
  json_output = JSON
@@ -3598,6 +3811,7 @@ async def main(argv):
3598
3811
  hostname = None
3599
3812
  serial_port = None
3600
3813
  baudrate = 115200
3814
+ repeater_mode = False
3601
3815
  timeout = 2
3602
3816
  pin = None
3603
3817
  first_device = False
@@ -3608,7 +3822,7 @@ async def main(argv):
3608
3822
  address = f.readline().strip()
3609
3823
 
3610
3824
  try:
3611
- opts, args = getopt.getopt(argv, "a:d:s:ht:p:b:fjDhvSlT:Pc:C")
3825
+ opts, args = getopt.getopt(argv, "a:d:s:ht:p:b:fjDhvSlT:Pc:Cr")
3612
3826
  except getopt.GetoptError:
3613
3827
  print("Unrecognized option, use -h to get more help")
3614
3828
  command_usage()
@@ -3620,6 +3834,8 @@ async def main(argv):
3620
3834
  process_event_message.color = False
3621
3835
  case "-C":
3622
3836
  interactive_loop.classic = not interactive_loop.classic
3837
+ case "-r": # repeater mode (raw text CLI)
3838
+ repeater_mode = True
3623
3839
  case "-d" : # name specified on cmdline
3624
3840
  address = arg
3625
3841
  case "-a" : # address specified on cmdline
@@ -3707,6 +3923,15 @@ async def main(argv):
3707
3923
  elif (json_output) :
3708
3924
  logger.setLevel(logging.ERROR)
3709
3925
 
3926
+ # Repeater mode - raw text CLI over serial
3927
+ if repeater_mode:
3928
+ if serial_port is None:
3929
+ print("Error: Repeater mode (-r) requires serial port (-s)")
3930
+ command_usage()
3931
+ return
3932
+ await repeater_loop(serial_port, baudrate)
3933
+ return
3934
+
3710
3935
  mc = None
3711
3936
  if not hostname is None : # connect via tcp
3712
3937
  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