feat(core/debuglink): emu batt state and events
What changed, and why it matters
This commit adds a debug-only feature to the Trezor firmware emulator that lets software tests fake the device's battery and charging state. It does not change real hardware behavior. The new function is only compiled into emulator builds and only when Python optimization is disabled (debug builds). It is intended for automated testing, not for end-user devices.
No immediate action required. Treat as a test/debug infrastructure change. If reviewing for release, confirm the new API is absent from production firmware builds (it should be excluded by TREZOR_EMULATOR/PYOPT guards).
Security signals we found
New debug/test-only API gated by TREZOR_EMULATOR and PYOPT==0 preprocessor conditions
No input validation beyond clamping SOC to 0-100 and treating negative values as 'unchanged'
Changes only emulator-specific C and MicroPython modules; no real hardware power-manager code modified
Adds pm_poll_init/deinit calls to emulator power manager lifecycle
Evidence from the diff
The patch introduces pm_set_emu_battery_state() in the Unix/emulator power manager and exposes it as trezorio.pm.set_emu_battery_state() in MicroPython when TREZOR_EMULATOR and PYOPT==0. It also wires up pm_poll_init/deinit in the emulator and adds a telemetry stub. The function accepts optional parameters for SOC, USB/wireless/NTC connection flags, charging limits, temperature control, and battery presence; -1/None leaves values unchanged. Charging and power status are derived from the supplied connection state. The SCons build now passes an emulator flag into the MicroPython preprocessing step.
Changed components
core/embed/io/power_manager/unix/power_manager.ccore/embed/upymod/modtrezorio/modtrezorio-pm.hcore/embed/projects/unix/main_main.ccore/embed/sec/telemetry/unix/telemetry.ccore/site_scons/models/T3W1/emulator.pycore/site_scons/site_tools/micropython/__init__.pycore/SConscript.firmwarecore/SConscript.unixcore/mocks/generated/trezorio/pm.pyiInspect captured patch +226 / −17
diff --git a/core/SConscript.firmware b/core/SConscript.firmware
index d8af60e6..1232c07d 100644
--- a/core/SConscript.firmware
+++ b/core/SConscript.firmware
@@ -870,6 +870,7 @@ if FROZEN:
telemetry=TELEMETRY,
n4w1=N4W1,
include_source_lines=MICROPY_ENABLE_SOURCE_LINE,
+ emulator=False,
)
source_mpyc = env.FrozenCFile(
diff --git a/core/SConscript.unix b/core/SConscript.unix
index 68085cfb..29e573bb 100644
--- a/core/SConscript.unix
+++ b/core/SConscript.unix
@@ -888,6 +888,7 @@ if FROZEN:
telemetry=TELEMETRY,
n4w1=N4W1,
include_source_lines=MICROPY_ENABLE_SOURCE_LINE,
+ emulator=True,
)
source_mpyc = env.FrozenCFile(
diff --git a/core/embed/io/power_manager/inc/io/power_manager.h b/core/embed/io/power_manager/inc/io/power_manager.h
index f6cd7985..123d8ab7 100644
--- a/core/embed/io/power_manager/inc/io/power_manager.h
+++ b/core/embed/io/power_manager/inc/io/power_manager.h
@@ -263,3 +263,14 @@ bool pm_driver_resume(void);
* @return true if the power manager is suspended, false otherwise
*/
bool pm_driver_is_suspended(void);
+
+#ifdef TREZOR_EMULATOR
+/**
+ * @brief Set emulated battery state with fine-grained control (emulator only).
+ * Pass -1 for any parameter to leave it unchanged.
+ */
+void pm_set_emu_battery_state(int soc, int usb_connected,
+ int wireless_connected, int ntc_connected,
+ int charging_limited, int temp_control_active,
+ int battery_connected);
+#endif
diff --git a/core/embed/io/power_manager/unix/power_manager.c b/core/embed/io/power_manager/unix/power_manager.c
index fe7e10eb..0554f7a4 100644
--- a/core/embed/io/power_manager/unix/power_manager.c
+++ b/core/embed/io/power_manager/unix/power_manager.c
@@ -26,9 +26,45 @@
#include <SDL.h>
#include <SDL2/SDL_events.h>
-pm_status_t pm_init(bool inherit_state) { return PM_OK; }
+#include "../power_manager_poll.h"
-void pm_deinit(void) {}
+typedef struct {
+ uint8_t soc;
+ pm_charging_status_t charging_status;
+ pm_power_status_t power_status;
+ bool usb_connected;
+ bool wireless_connected;
+ bool ntc_connected;
+ bool charging_limited;
+ bool temp_control_active;
+ bool battery_connected;
+} emu_battery_state_t;
+
+static const emu_battery_state_t emu_battery_default = {
+ .soc = 100,
+ .charging_status = PM_BATTERY_DISCHARGING,
+ .power_status = PM_STATE_ACTIVE,
+ .usb_connected = false,
+ .wireless_connected = false,
+ .ntc_connected = true,
+ .charging_limited = false,
+ .temp_control_active = false,
+ .battery_connected = true,
+};
+
+static emu_battery_state_t emu_battery = emu_battery_default;
+
+pm_status_t pm_init(bool inherit_state) {
+ if (!inherit_state) {
+ emu_battery = emu_battery_default;
+ }
+ if (!pm_poll_init()) {
+ return PM_ERROR;
+ }
+ return PM_OK;
+}
+
+void pm_deinit(void) { pm_poll_deinit(); }
pm_status_t pm_hibernate(void) {
exit(1);
@@ -58,24 +94,86 @@ pm_status_t pm_turn_on(void) { return PM_OK; }
pm_status_t pm_charging_enable(void) { return PM_OK; }
pm_status_t pm_charging_disable(void) { return PM_OK; }
-bool pm_get_events(pm_event_t* event_flags) {
- memset(event_flags, 0, sizeof(pm_event_t));
- return false;
-}
-
pm_status_t pm_get_state(pm_state_t* state) {
- state->usb_connected = true;
- state->wireless_connected = false;
- state->charging_status = PM_BATTERY_IDLE;
- state->power_status = PM_STATE_ACTIVE;
- state->soc = 100;
- state->ntc_connected = true;
- state->battery_connected = true;
+ state->usb_connected = emu_battery.usb_connected;
+ state->wireless_connected = emu_battery.wireless_connected;
+ state->ntc_connected = emu_battery.ntc_connected;
+ state->charging_limited = emu_battery.charging_limited;
+ state->temp_control_active = emu_battery.temp_control_active;
+ state->battery_connected = emu_battery.battery_connected;
+ state->charging_status = emu_battery.charging_status;
+ state->power_status = emu_battery.power_status;
+ state->soc = emu_battery.soc;
+ // unused fields in emulator so we mock them to 3.7V and 20˚C
+ state->battery_ocv = 3.7f;
+ state->battery_temp = 20.0f;
return PM_OK;
}
-bool pm_is_charging(void) { return false; }
+bool pm_is_charging(void) {
+ return emu_battery.charging_status == PM_BATTERY_CHARGING;
+}
-bool pm_usb_connected(void) { return true; }
+bool pm_usb_is_connected(void) { return emu_battery.usb_connected; }
pm_status_t pm_set_soc_target(uint8_t target) { return PM_OK; }
+
+// Derive charging status from connection state
+static pm_charging_status_t emu_derive_charging_status(void) {
+ if (!emu_battery.battery_connected) {
+ return PM_BATTERY_IDLE;
+ }
+ if (emu_battery.usb_connected || emu_battery.wireless_connected) {
+ return PM_BATTERY_CHARGING;
+ }
+ return PM_BATTERY_DISCHARGING;
+}
+
+// Derive power status from connection state
+static pm_power_status_t emu_derive_power_status(void) {
+ if (emu_battery.usb_connected || emu_battery.wireless_connected) {
+ return PM_STATE_ACTIVE;
+ }
+ if (emu_battery.battery_connected) {
+ return PM_STATE_ACTIVE;
+ }
+ return PM_STATE_HIBERNATE;
+}
+
+void pm_set_emu_battery_state(int soc, int usb_connected,
+ int wireless_connected, int ntc_connected,
+ int charging_limited, int temp_control_active,
+ int battery_connected) {
+ // Apply only fields that are set (>= 0 means set, -1 means unset/None)
+ if (soc >= 0) {
+ emu_battery.soc = (uint8_t)(soc > 100 ? 100 : soc);
+ }
+
+ if (usb_connected >= 0) {
+ emu_battery.usb_connected = (bool)usb_connected;
+ }
+
+ if (wireless_connected >= 0) {
+ emu_battery.wireless_connected = (bool)wireless_connected;
+ }
+
+ if (ntc_connected >= 0) {
+ emu_battery.ntc_connected = (bool)ntc_connected;
+ }
+
+ if (charging_limited >= 0) {
+ emu_battery.charging_limited = (bool)charging_limited;
+ }
+
+ if (temp_control_active >= 0) {
+ emu_battery.temp_control_active = (bool)temp_control_active;
+ }
+
+ if (battery_connected >= 0) {
+ emu_battery.battery_connected = (bool)battery_connected;
+ }
+
+ // Always derive charging and power status from connection state
+ emu_battery.charging_status = emu_derive_charging_status();
+ emu_battery.power_status = emu_derive_power_status();
+}
diff --git a/core/embed/projects/unix/main_main.c b/core/embed/projects/unix/main_main.c
index 2c9686e8..3d1c96c8 100644
--- a/core/embed/projects/unix/main_main.c
+++ b/core/embed/projects/unix/main_main.c
@@ -46,6 +46,10 @@
#include <io/ble.h>
#endif
+#ifdef USE_POWER_MANAGER
+#include <io/power_manager.h>
+#endif
+
#ifdef USE_TOUCH
#include <io/touch.h>
#endif
@@ -92,6 +96,10 @@ static void drivers_init(void) {
ble_init();
#endif
+#ifdef USE_POWER_MANAGER
+ pm_init(true);
+#endif
+
#ifdef USE_APP_LOADING
app_cache_init();
app_loader_init();
diff --git a/core/embed/sec/telemetry/unix/telemetry.c b/core/embed/sec/telemetry/unix/telemetry.c
index f9bb8464..b75be074 100644
--- a/core/embed/sec/telemetry/unix/telemetry.c
+++ b/core/embed/sec/telemetry/unix/telemetry.c
@@ -34,4 +34,8 @@ void telemetry_reset(void) {
// No-op for emulator
}
+void telemetry_update_battery_errors(telemetry_batt_errors_t errors) {
+ // No-op for emulator
+}
+
#endif
diff --git a/core/embed/upymod/modtrezorio/modtrezorio-pm.h b/core/embed/upymod/modtrezorio/modtrezorio-pm.h
index cf53b23e..89f8d19a 100644
--- a/core/embed/upymod/modtrezorio/modtrezorio-pm.h
+++ b/core/embed/upymod/modtrezorio/modtrezorio-pm.h
@@ -122,6 +122,66 @@ STATIC mp_obj_t mod_trezorio_pm_is_wireless_connected() {
STATIC MP_DEFINE_CONST_FUN_OBJ_0(mod_trezorio_pm_is_wireless_connected_obj,
mod_trezorio_pm_is_wireless_connected);
+#ifdef TREZOR_EMULATOR
+#if PYOPT == 0
+/// def set_emu_battery_state(
+/// 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 with fine-grained control.
+/// Only available on emulator. Pass None to leave a field unchanged.
+/// Charging status and power status are derived from connection states.
+/// """
+STATIC mp_obj_t mod_trezorio_pm_set_emu_battery_state(size_t n_args,
+ const mp_obj_t* pos_args,
+ mp_map_t* kw_args) {
+ static const mp_arg_t allowed_args[] = {
+ {MP_QSTR_soc, MP_ARG_OBJ, {.u_rom_obj = MP_ROM_NONE}},
+ {MP_QSTR_usb_connected, MP_ARG_OBJ, {.u_rom_obj = MP_ROM_NONE}},
+ {MP_QSTR_wireless_connected, MP_ARG_OBJ, {.u_rom_obj = MP_ROM_NONE}},
+ {MP_QSTR_ntc_connected, MP_ARG_OBJ, {.u_rom_obj = MP_ROM_NONE}},
+ {MP_QSTR_charging_limited, MP_ARG_OBJ, {.u_rom_obj = MP_ROM_NONE}},
+ {MP_QSTR_temp_control_active, MP_ARG_OBJ, {.u_rom_obj = MP_ROM_NONE}},
+ {MP_QSTR_battery_connected, MP_ARG_OBJ, {.u_rom_obj = MP_ROM_NONE}},
+ };
+
+ mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
+ mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args),
+ allowed_args, args);
+
+ // Convert: None -> -1, otherwise get int/bool value
+ int soc =
+ (args[0].u_obj == mp_const_none) ? -1 : mp_obj_get_int(args[0].u_obj);
+ int usb_connected =
+ (args[1].u_obj == mp_const_none) ? -1 : mp_obj_is_true(args[1].u_obj);
+ int wireless_connected =
+ (args[2].u_obj == mp_const_none) ? -1 : mp_obj_is_true(args[2].u_obj);
+ int ntc_connected =
+ (args[3].u_obj == mp_const_none) ? -1 : mp_obj_is_true(args[3].u_obj);
+ int charging_limited =
+ (args[4].u_obj == mp_const_none) ? -1 : mp_obj_is_true(args[4].u_obj);
+ int temp_control_active =
+ (args[5].u_obj == mp_const_none) ? -1 : mp_obj_is_true(args[5].u_obj);
+ int battery_connected =
+ (args[6].u_obj == mp_const_none) ? -1 : mp_obj_is_true(args[6].u_obj);
+
+ pm_set_emu_battery_state(soc, usb_connected, wireless_connected,
+ ntc_connected, charging_limited, temp_control_active,
+ battery_connected);
+
+ return mp_const_none;
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_KW(mod_trezorio_pm_set_emu_battery_state_obj, 0,
+ mod_trezorio_pm_set_emu_battery_state);
+#endif
+#endif
+
STATIC const mp_rom_map_elem_t mod_trezorio_pm_globals_table[] = {
{MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_pm)},
{MP_ROM_QSTR(MP_QSTR_soc), MP_ROM_PTR(&mod_trezorio_pm_soc_obj)},
@@ -132,6 +192,12 @@ STATIC const mp_rom_map_elem_t mod_trezorio_pm_globals_table[] = {
MP_ROM_PTR(&mod_trezorio_pm_is_usb_connected_obj)},
{MP_ROM_QSTR(MP_QSTR_is_wireless_connected),
MP_ROM_PTR(&mod_trezorio_pm_is_wireless_connected_obj)},
+#ifdef TREZOR_EMULATOR
+#if PYOPT == 0
+ {MP_ROM_QSTR(MP_QSTR_set_emu_battery_state),
+ MP_ROM_PTR(&mod_trezorio_pm_set_emu_battery_state_obj)},
+#endif
+#endif
// Wakeup flag constants
{MP_ROM_QSTR(MP_QSTR_WAKEUP_FLAG_BUTTON), MP_ROM_INT(WAKEUP_FLAG_BUTTON)},
@@ -161,5 +227,5 @@ STATIC MP_DEFINE_CONST_DICT(mod_trezorio_pm_globals,
STATIC const mp_obj_module_t mod_trezorio_pm_module = {
.base = {&mp_type_module},
- .globals = (mp_obj_dict_t *)&mod_trezorio_pm_globals,
+ .globals = (mp_obj_dict_t*)&mod_trezorio_pm_globals,
};
diff --git a/core/mocks/generated/trezorio/pm.pyi b/core/mocks/generated/trezorio/pm.pyi
index 16d25926..e91a83e8 100644
--- a/core/mocks/generated/trezorio/pm.pyi
+++ b/core/mocks/generated/trezorio/pm.pyi
@@ -59,3 +59,20 @@ def is_wireless_connected() -> bool:
Returns True if Wireless power source is connected, False otherwise.
Raises RuntimeError on failure.
"""
+
+
+# upymod/modtrezorio/modtrezorio-pm.h
+def set_emu_battery_state(
+ 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 with fine-grained control.
+ Only available on emulator. Pass None to leave a field unchanged.
+ Charging status and power status are derived from connection states.
+ """
diff --git a/core/site_scons/models/T3W1/emulator.py b/core/site_scons/models/T3W1/emulator.py
index 171b2d76..28bfc166 100644
--- a/core/site_scons/models/T3W1/emulator.py
+++ b/core/site_scons/models/T3W1/emulator.py
@@ -132,6 +132,7 @@ def configure(
sources += [
"embed/io/power_manager/unix/power_manager.c",
+ "embed/io/power_manager/power_manager_poll.c",
]
defines += [("USE_POWER_MANAGER", "1")]
paths += ["embed/io/power_manager/inc"]
diff --git a/core/site_scons/site_tools/micropython/__init__.py b/core/site_scons/site_tools/micropython/__init__.py
index eb18dea1..90d45949 100644
--- a/core/site_scons/site_tools/micropython/__init__.py
+++ b/core/site_scons/site_tools/micropython/__init__.py
@@ -53,6 +53,7 @@ def generate(env):
ble = env["use_ble"]
btc_only = env["bitcoin_only"] == "1"
button = env["use_button"]
+ emulator = env["emulator"]
haptic = env["use_haptic"]
layout_bolt = env["ui_layout"] == "UI_LAYOUT_BOLT"
layout_caesar = env["ui_layout"] == "UI_LAYOUT_CAESAR"
@@ -74,6 +75,7 @@ def generate(env):
rf"-e 's/utils\.BITCOIN_ONLY/{btc_only}/g'",
rf"-e 's/utils\.USE_BUTTON/{button}/g'",
rf"-e 's/utils\.USE_HAPTIC/{haptic}/g'",
+ rf"-e 's/utils\.EMULATOR/{emulator}/g'",
rf"-e 's/utils\.UI_LAYOUT == \"BOLT\"/{layout_bolt}/g'",
rf"-e 's/utils\.UI_LAYOUT == \"CAESAR\"/{layout_caesar}/g'",
rf"-e 's/utils\.UI_LAYOUT == \"DELIZIA\"/{layout_delizia}/g'",
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.