Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,42 @@ This feature only really works in interactive mode.

Note: There is also an `auto_update_contacts` setting that has nothing to do with adding contacts, it permits to automatically sync contact lists between device and meshcore-cli (when there is an update in name, location or path).

### LED Control

The BLE and status LEDs on companion devices can be controlled using the `set` and `get` commands.

#### BLE LED (`led_ble_mode`)

Controls the BLE connection indicator LED.

| Value | Behavior |
|-------|----------|
| 0 | Normal: blinks when disconnected, solid when connected |
| 1 | Blinks only when disconnected from BLE |
| 2 | Solid only when connected to BLE |
| 3 | Disabled |

```
set led_ble_mode 1
get led_ble_mode
```

#### Status LED (`led_status_mode`)

Controls the status LED blinking.

| Value | Behavior |
|-------|----------|
| 0 | Enabled (normal blinking) |
| 1 | Disabled |

```
set led_status_mode 1
get led_status_mode
```

Note: LED settings are persisted to the device and require a reboot to take effect.

### Issuing batch commands to contacts with apply to

`apply_to <f> <cmd>` : applies cmd to contacts matching filter `<f>` it can be used to apply the same command to a pool of repeaters, or remove some contacts matching a condition.
Expand Down
61 changes: 61 additions & 0 deletions src/meshcore_cli/meshcore_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,8 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
"max_flood_attempts" : None,
"flood_after" : None,
"path_hash_mode": None,
"led_ble_mode": None,
"led_status_mode": None,
},
"get" : {"name":None,
"bat":None,
Expand Down Expand Up @@ -620,6 +622,8 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
"stats_packets":None,
"allowed_repeat_freq":None,
"path_hash_mode":None,
"led_ble_mode":None,
"led_status_mode":None,
},
"?get":None,
"?set":None,
Expand Down Expand Up @@ -2055,6 +2059,30 @@ async def next_cmd(mc, cmds, json_output=False):
print(json.dumps(res.payload, indent=4))
else:
print("ok")
case "led_ble_mode":
mode = int(cmds[2])
if mode < 0 or mode > 3:
logger.error("led_ble_mode must be 0-3 (0=enabled, 1=disconn_only, 2=conn_only, 3=disabled)")
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error text for led_ble_mode uses a different set of labels than the help/README ("0=enabled, 1=disconn_only, 2=conn_only" vs "0=normal, 1=disconn blink only, 2=conn solid only"). Please align terminology so users see consistent meanings across set validation errors and ?set/README docs.

Suggested change
logger.error("led_ble_mode must be 0-3 (0=enabled, 1=disconn_only, 2=conn_only, 3=disabled)")
logger.error("led_ble_mode must be 0-3 (0=normal, 1=disconn blink only, 2=conn solid only, 3=disabled)")

Copilot uses AI. Check for mistakes.
Comment on lines +2062 to +2065
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mode = int(cmds[2]) can raise ValueError for non-integer input, and next_cmd() does not catch ValueError (only IndexError/EOFError/KeyboardInterrupt). This will crash the CLI instead of producing a user-facing error; wrap the conversion in a try/except ValueError and report an invalid value message.

Copilot uses AI. Check for mistakes.
else:
res = await mc.commands.set_led_ble_mode(mode)
if res.type == EventType.ERROR:
print(f"Error: {res}")
elif json_output:
print(json.dumps(res.payload, indent=4))
else:
print("ok")
case "led_status_mode":
mode = int(cmds[2])
if mode < 0 or mode > 1:
logger.error("led_status_mode must be 0-1 (0=enabled, 1=disabled)")
Comment on lines +2074 to +2077
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue here: mode = int(cmds[2]) may raise ValueError and crash the command loop. Handle invalid/non-numeric values explicitly (e.g., try/except ValueError) so the CLI prints/logs a clear error instead of exiting.

Copilot uses AI. Check for mistakes.
else:
res = await mc.commands.set_led_status_mode(mode)
if res.type == EventType.ERROR:
print(f"Error: {res}")
elif json_output:
print(json.dumps(res.payload, indent=4))
else:
print("ok")
Comment on lines +2067 to +2085
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These calls assume the installed meshcore library exposes set_led_ble_mode / set_led_status_mode. If this CLI is run with an older meshcore version, this will raise AttributeError at runtime. Consider guarding with hasattr (and printing a helpful message about required firmware/library versions) and/or updating the minimum meshcore dependency to the first release that includes these commands.

Suggested change
res = await mc.commands.set_led_ble_mode(mode)
if res.type == EventType.ERROR:
print(f"Error: {res}")
elif json_output:
print(json.dumps(res.payload, indent=4))
else:
print("ok")
case "led_status_mode":
mode = int(cmds[2])
if mode < 0 or mode > 1:
logger.error("led_status_mode must be 0-1 (0=enabled, 1=disabled)")
else:
res = await mc.commands.set_led_status_mode(mode)
if res.type == EventType.ERROR:
print(f"Error: {res}")
elif json_output:
print(json.dumps(res.payload, indent=4))
else:
print("ok")
if not hasattr(mc.commands, "set_led_ble_mode"):
msg = (
"Current meshcore library/firmware does not support 'led_ble_mode'. "
"Please update to a version that includes the 'set_led_ble_mode' command."
)
logger.error(msg)
print(msg)
else:
res = await mc.commands.set_led_ble_mode(mode)
if res.type == EventType.ERROR:
print(f"Error: {res}")
elif json_output:
print(json.dumps(res.payload, indent=4))
else:
print("ok")
case "led_status_mode":
mode = int(cmds[2])
if mode < 0 or mode > 1:
logger.error("led_status_mode must be 0-1 (0=enabled, 1=disabled)")
else:
if not hasattr(mc.commands, "set_led_status_mode"):
msg = (
"Current meshcore library/firmware does not support 'led_status_mode'. "
"Please update to a version that includes the 'set_led_status_mode' command."
)
logger.error(msg)
print(msg)
else:
res = await mc.commands.set_led_status_mode(mode)
if res.type == EventType.ERROR:
print(f"Error: {res}")
elif json_output:
print(json.dumps(res.payload, indent=4))
else:
print("ok")

Copilot uses AI. Check for mistakes.
case "name":
res = await mc.commands.set_name(cmds[2])
logger.debug(res)
Expand Down Expand Up @@ -2380,6 +2408,30 @@ async def next_cmd(mc, cmds, json_output=False):
print(f"{res.payload['path_hash_mode']}")
else:
print("Not available")
case "led_ble_mode":
res = await mc.commands.send_device_query()
logger.debug(res)
if res.type == EventType.ERROR :
print(f"ERROR: {res}")
elif json_output :
print(json.dumps(res.payload, indent=4))
else :
if "led_ble_mode" in res.payload :
print(f"{res.payload['led_ble_mode']}")
else:
print("Not available")
case "led_status_mode":
res = await mc.commands.send_device_query()
logger.debug(res)
if res.type == EventType.ERROR :
print(f"ERROR: {res}")
elif json_output :
print(json.dumps(res.payload, indent=4))
else :
if "led_status_mode" in res.payload :
print(f"{res.payload['led_status_mode']}")
else:
print("Not available")
case "bat" :
res = await mc.commands.get_bat()
logger.debug(res)
Expand Down Expand Up @@ -3728,6 +3780,8 @@ def get_help_for (cmdname, context="line") :
stats_packets : packets stats (recv/sent/flood/direct)
allowed_repeat_freq: possible frequency ranges for repeater mode
path_hash_mode
led_ble_mode : BLE LED mode (0-3)
led_status_mode : status LED mode (0-1)
""")

elif cmdname == "set" :
Expand All @@ -3753,6 +3807,10 @@ def get_help_for (cmdname, context="line") :
(pending contacts list is built by meshcli from adverts while connected)
autoadd_config : set autoadd_config flags (see ?autoadd)
path_hash_mode <value>
led_ble_mode <0-3> : BLE LED behavior
0=normal, 1=disconn blink only, 2=conn solid only, 3=disabled
led_status_mode <0-1> : status LED behavior
0=enabled (blinking), 1=disabled
display:
print_timestamp <on/off/fmt>: toggle printing of timestamp, can be strftime format
print_snr <on/off> : toggle snr display in messages
Expand Down Expand Up @@ -3882,6 +3940,7 @@ def get_help_for (cmdname, context="line") :
"bridge.channel": None, "bridge.secret": None, "bridge.type": None,
"adc.multiplier": None, "acl": None,
"owner.info": None,
"led.ble": None, "led.status": None,
},
"set": {
"name": None, "radio": None, "tx": None, "freq": None,
Expand All @@ -3897,6 +3956,8 @@ def get_help_for (cmdname, context="line") :
"bridge.baud": None, "bridge.channel": None, "bridge.secret": None,
"adc.multiplier": None,
"owner.info": None,
"led.ble": None,
"led.status": None,
},
"powersaving": {"on":None, "off":None,},
"password": None,
Expand Down
Loading