feat(python): set battery state function and CLI
What changed, and why it matters
This commit adds a new Python helper and command-line tool to control the emulated battery state on Trezor hardware simulators (emulators). It does not change firmware running on real devices. The feature is intended for testing only and is gated behind the existing debug-link mechanism, which already requires special debug access to the device or emulator.
No immediate security action is required. Treat this as a normal feature addition for testing/QA. Ensure the debug-link interface remains disabled in production firmware builds and that emulator-only commands are not exposed to end-user documentation as operational features.
Security signals we found
New debug-only functionality that manipulates emulated power/battery state
Requires debug transport access, which is already a privileged testing interface
Explicitly documented as emulator-only; no real-device battery control
No input validation beyond an SoC integer range of 0-100
No changelog entry, but commit title clearly describes feature addition
Evidence from the diff
The patch introduces DebugLink.set_battery_state() in python/src/trezorlib/debuglink.py and a matching trezorctl debug set-battery-state CLI in python/src/trezorlib/cli/debug.py. The function sends a DebugLinkSetBatteryState protobuf message with optional fields (state-of-charge percentage, USB/wireless/NTC connection flags, charging-limited, temperature-control-active, battery-connected). The CLI validates the SoC as 0-100 and passes only explicitly provided values. Both the library method and CLI docstrings state this works on the emulator only. It reuses the existing debug transport opened via session.client.transport.find_debug().
Changed components
python/src/trezorlib/debuglink.pypython/src/trezorlib/cli/debug.pytrezorctl debug subcommandInspect captured patch +118 / −0
diff --git a/python/src/trezorlib/cli/debug.py b/python/src/trezorlib/cli/debug.py
index 0d897a78..6e7ccba4 100644
--- a/python/src/trezorlib/cli/debug.py
+++ b/python/src/trezorlib/cli/debug.py
@@ -135,3 +135,93 @@ def set_log_filter(session: "Session", filter: str) -> None:
debug = DebugLink(transport=debug_transport)
debuglink_set_log_filter(debug, filter)
debug_transport.close()
+
+
+@cli.command()
+@click.option(
+ "--soc",
+ type=click.IntRange(0, 100),
+ default=None,
+ help="State of charge percentage (0-100)",
+)
+@click.option(
+ "--usb/--no-usb", "usb_connected", default=None, help="USB cable connected"
+)
+@click.option(
+ "--wireless/--no-wireless",
+ "wireless_connected",
+ default=None,
+ help="Wireless charger connected",
+)
+@click.option(
+ "--ntc/--no-ntc", "ntc_connected", default=None, help="Temperature sensor connected"
+)
+@click.option(
+ "--charging-limited/--no-charging-limited",
+ default=None,
+ help="Charging current is limited",
+)
+@click.option(
+ "--temp-control/--no-temp-control",
+ "temp_control_active",
+ default=None,
+ help="Temperature control active",
+)
+@click.option(
+ "--battery/--no-battery",
+ "battery_connected",
+ default=None,
+ help="Battery physically connected",
+)
+@with_session(seedless=True)
+def set_battery_state(
+ session: "Session",
+ soc: int | None,
+ usb_connected: bool | None,
+ wireless_connected: bool | None,
+ ntc_connected: bool | None,
+ charging_limited: bool | None,
+ temp_control_active: bool | None,
+ battery_connected: bool | None,
+) -> None:
+ """Set emulated battery/power state (emulator only).
+
+ All options are optional — only specified values are changed.
+ Charging status and power status are derived from connection states.
+
+ Examples:
+
+ trezorctl debug set-battery-state --soc 50 --usb
+
+ trezorctl debug set-battery-state --no-usb --wireless --soc 30
+
+ trezorctl debug set-battery-state --no-battery
+ """
+ debug_transport = session.client.transport.find_debug()
+ debug_transport.open()
+ debug = DebugLink(transport=debug_transport)
+ flags = dict(
+ usb_connected=usb_connected,
+ wireless_connected=wireless_connected,
+ ntc_connected=ntc_connected,
+ charging_limited=charging_limited,
+ temp_control_active=temp_control_active,
+ battery_connected=battery_connected,
+ )
+ debug.set_battery_state(soc=soc, **flags)
+ debug_transport.close()
+
+ parts = []
+ if soc is not None:
+ parts.append(f"soc={soc}%")
+
+ if usb_connected is not None:
+ parts.append(f"usb={'on' if usb_connected else 'off'}")
+ for name, value in flags.items():
+ if value is not None:
+ parts.append(f"{name}={'on' if value else 'off'}")
+
+ if parts:
+ click.echo(f"Battery state updated: {', '.join(parts)}")
+ else:
+ click.echo("No battery state parameters specified. Nothing changed.")
diff --git a/python/src/trezorlib/debuglink.py b/python/src/trezorlib/debuglink.py
index fe588803..1de9f707 100644
--- a/python/src/trezorlib/debuglink.py
+++ b/python/src/trezorlib/debuglink.py
@@ -908,6 +908,34 @@ class DebugLink:
messages.DebugLinkEraseSdCard(format=format), expect=messages.Success
)
+ def set_battery_state(
+ self,
+ soc: int | None = None,
+ usb_connected: bool | None = None,
+ wireless_connected: bool | None = None,
+ ntc_connected: bool | None = None,
+ charging_limited: bool | None = None,
+ temp_control_active: bool | None = None,
+ battery_connected: bool | None = None,
+ ) -> None:
+ """Set emulated battery/power state. Only works on emulator.
+
+ All parameters are optional — pass only what you want to change.
+ Charging status and power status are derived from connection states.
+ """
+ self._call(
+ messages.DebugLinkSetBatteryState(
+ soc=soc,
+ usb_connected=usb_connected,
+ wireless_connected=wireless_connected,
+ ntc_connected=ntc_connected,
+ charging_limited=charging_limited,
+ temp_control_active=temp_control_active,
+ battery_connected=battery_connected,
+ ),
+ expect=messages.Success,
+ )
+
def snapshot_legacy(self) -> None:
"""Snapshot the current state of the device."""
if self.model is not models.T1B1:
Why this scored 21/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.