feat(core): add battery_cycle field in the protobuf telemetry message.
What changed, and why it matters
This commit adds a new data field called battery_cycles to the device's telemetry report. It simply reads an existing stored battery-cycle counter and includes it in the telemetry message sent to connected software. There is no indication this change fixes a bug, changes access controls, or introduces a security vulnerability.
No security action required. Treat as a normal feature addition.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change extends the Telemetry protobuf message with an optional sint32 battery_cycles field (field number 4), updates the MicroPython C module telemetry_get() to return a 4-tuple instead of a 3-tuple, and propagates the new value through the Python telemetry app and generated Rust/protobuf bindings. The value is read from existing stored telemetry data (data.battery_cycles) and multiplied by 1000.0f, matching the pattern used for temperature values. No parsing, allocation, or trust-boundary logic is modified beyond adding the new field.
Changed components
common/protob/messages-telemetry.protocore/embed/upymod/modtrezorutils/modtrezorutils.ccore/mocks/generated/trezorutils.pyicore/src/apps/telemetry/get_telemetry.pycore/src/trezor/messages.pyrust/trezor-client/src/protos/generated/messages_telemetry.rsInspect captured patch +55 / −12
diff --git a/common/protob/messages-telemetry.proto b/common/protob/messages-telemetry.proto
index 17e215787..8dd5559f5 100644
--- a/common/protob/messages-telemetry.proto
+++ b/common/protob/messages-telemetry.proto
@@ -25,5 +25,6 @@ message TelemetryGet {
message Telemetry {
optional sint32 min_temp_c = 1; // Minimum recorded battery temperature in milli-Celsius
optional sint32 max_temp_c = 2; // Maximum recorded battery temperature in milli-Celsius
- optional uint32 battery_errors = 3; // Bitfield of battery errors
+ optional uint32 battery_errors = 3; // Bitfield of battery errors
+ optional sint32 battery_cycles = 4; // Total battery cycles
}
diff --git a/core/embed/upymod/modtrezorutils/modtrezorutils.c b/core/embed/upymod/modtrezorutils/modtrezorutils.c
index 2db078fe5..57bba05ab 100644
--- a/core/embed/upymod/modtrezorutils/modtrezorutils.c
+++ b/core/embed/upymod/modtrezorutils/modtrezorutils.c
@@ -68,7 +68,7 @@
/// from trezor import utils
#ifdef USE_TELEMETRY
-/// def telemetry_get() -> tuple[int, int, int] | None:
+/// def telemetry_get() -> tuple[int, int, int, int] | None:
/// """
/// Retrieves the stored telemetry data. Returns a tuple
/// (min_temp_milli_c, max_temp_milli_c, battery_errors)
@@ -80,12 +80,13 @@ STATIC mp_obj_t mod_trezorutils_telemetry_get(void) {
return mp_const_none;
}
- mp_obj_t tuple[3];
+ mp_obj_t tuple[4];
tuple[0] = mp_obj_new_int((int32_t)(data.min_temp_c * 1000.0f));
tuple[1] = mp_obj_new_int((int32_t)(data.max_temp_c * 1000.0f));
tuple[2] = mp_obj_new_int(data.battery_errors.all);
+ tuple[3] = mp_obj_new_int((int32_t)(data.battery_cycles *1000.0f));
- return mp_obj_new_tuple(3, tuple);
+ return mp_obj_new_tuple(4, tuple);
}
STATIC MP_DEFINE_CONST_FUN_OBJ_0(mod_trezorutils_telemetry_get_obj,
mod_trezorutils_telemetry_get);
diff --git a/core/mocks/generated/trezorutils.pyi b/core/mocks/generated/trezorutils.pyi
index 942b2a8bc..299024661 100644
--- a/core/mocks/generated/trezorutils.pyi
+++ b/core/mocks/generated/trezorutils.pyi
@@ -11,7 +11,7 @@ from trezor import utils
# upymod/modtrezorutils/modtrezorutils.c
-def telemetry_get() -> tuple[int, int, int] | None:
+def telemetry_get() -> tuple[int, int, int, int] | None:
"""
Retrieves the stored telemetry data. Returns a tuple
(min_temp_milli_c, max_temp_milli_c, battery_errors)
diff --git a/core/src/apps/telemetry/get_telemetry.py b/core/src/apps/telemetry/get_telemetry.py
index 143b061c6..730127dce 100644
--- a/core/src/apps/telemetry/get_telemetry.py
+++ b/core/src/apps/telemetry/get_telemetry.py
@@ -10,11 +10,12 @@ async def get_telemetry(msg: TelemetryGet) -> Telemetry:
data = telemetry_get()
if data:
- min_temp_c, max_temp_c, battery_errors = data
+ min_temp_c, max_temp_c, battery_errors, battery_cycles = data
return Telemetry(
min_temp_c=min_temp_c,
max_temp_c=max_temp_c,
battery_errors=battery_errors,
+ battery_cycles=battery_cycles,
)
else:
return Telemetry()
diff --git a/core/src/trezor/messages.py b/core/src/trezor/messages.py
index fad99ea4f..743e578fe 100644
--- a/core/src/trezor/messages.py
+++ b/core/src/trezor/messages.py
@@ -6138,6 +6138,7 @@ if TYPE_CHECKING:
min_temp_c: "int | None"
max_temp_c: "int | None"
battery_errors: "int | None"
+ battery_cycles: "int | None"
def __init__(
self,
@@ -6145,6 +6146,7 @@ if TYPE_CHECKING:
min_temp_c: "int | None" = None,
max_temp_c: "int | None" = None,
battery_errors: "int | None" = None,
+ battery_cycles: "int | None" = None,
) -> None:
pass
diff --git a/rust/trezor-client/src/protos/generated/messages_telemetry.rs b/rust/trezor-client/src/protos/generated/messages_telemetry.rs
index 71dabec23..aedd9ea18 100644
--- a/rust/trezor-client/src/protos/generated/messages_telemetry.rs
+++ b/rust/trezor-client/src/protos/generated/messages_telemetry.rs
@@ -137,6 +137,8 @@ pub struct Telemetry {
pub max_temp_c: ::std::option::Option<i32>,
// @@protoc_insertion_point(field:hw.trezor.messages.telemetry.Telemetry.battery_errors)
pub battery_errors: ::std::option::Option<u32>,
+ // @@protoc_insertion_point(field:hw.trezor.messages.telemetry.Telemetry.battery_cycles)
+ pub battery_cycles: ::std::option::Option<i32>,
// special fields
// @@protoc_insertion_point(special_field:hw.trezor.messages.telemetry.Telemetry.special_fields)
pub special_fields: ::protobuf::SpecialFields,
@@ -210,8 +212,27 @@ impl Telemetry {
self.battery_errors = ::std::option::Option::Some(v);
}
+ // optional sint32 battery_cycles = 4;
+
+ pub fn battery_cycles(&self) -> i32 {
+ self.battery_cycles.unwrap_or(0)
+ }
+
+ pub fn clear_battery_cycles(&mut self) {
+ self.battery_cycles = ::std::option::Option::None;
+ }
+
+ pub fn has_battery_cycles(&self) -> bool {
+ self.battery_cycles.is_some()
+ }
+
+ // Param is passed by value, moved
+ pub fn set_battery_cycles(&mut self, v: i32) {
+ self.battery_cycles = ::std::option::Option::Some(v);
+ }
+
fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData {
- let mut fields = ::std::vec::Vec::with_capacity(3);
+ let mut fields = ::std::vec::Vec::with_capacity(4);
let mut oneofs = ::std::vec::Vec::with_capacity(0);
fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>(
"min_temp_c",
@@ -228,6 +249,11 @@ impl Telemetry {
|m: &Telemetry| { &m.battery_errors },
|m: &mut Telemetry| { &mut m.battery_errors },
));
+ fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>(
+ "battery_cycles",
+ |m: &Telemetry| { &m.battery_cycles },
+ |m: &mut Telemetry| { &mut m.battery_cycles },
+ ));
::protobuf::reflect::GeneratedMessageDescriptorData::new_2::<Telemetry>(
"Telemetry",
fields,
@@ -255,6 +281,9 @@ impl ::protobuf::Message for Telemetry {
24 => {
self.battery_errors = ::std::option::Option::Some(is.read_uint32()?);
},
+ 32 => {
+ self.battery_cycles = ::std::option::Option::Some(is.read_sint32()?);
+ },
tag => {
::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?;
},
@@ -276,6 +305,9 @@ impl ::protobuf::Message for Telemetry {
if let Some(v) = self.battery_errors {
my_size += ::protobuf::rt::uint32_size(3, v);
}
+ if let Some(v) = self.battery_cycles {
+ my_size += ::protobuf::rt::sint32_size(4, v);
+ }
my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields());
self.special_fields.cached_size().set(my_size as u32);
my_size
@@ -291,6 +323,9 @@ impl ::protobuf::Message for Telemetry {
if let Some(v) = self.battery_errors {
os.write_uint32(3, v)?;
}
+ if let Some(v) = self.battery_cycles {
+ os.write_sint32(4, v)?;
+ }
os.write_unknown_fields(self.special_fields.unknown_fields())?;
::std::result::Result::Ok(())
}
@@ -311,6 +346,7 @@ impl ::protobuf::Message for Telemetry {
self.min_temp_c = ::std::option::Option::None;
self.max_temp_c = ::std::option::Option::None;
self.battery_errors = ::std::option::Option::None;
+ self.battery_cycles = ::std::option::Option::None;
self.special_fields.clear();
}
@@ -319,6 +355,7 @@ impl ::protobuf::Message for Telemetry {
min_temp_c: ::std::option::Option::None,
max_temp_c: ::std::option::Option::None,
battery_errors: ::std::option::Option::None,
+ battery_cycles: ::std::option::Option::None,
special_fields: ::protobuf::SpecialFields::new(),
};
&instance
@@ -344,11 +381,12 @@ impl ::protobuf::reflect::ProtobufValue for Telemetry {
static file_descriptor_proto_data: &'static [u8] = b"\
\n\x18messages-telemetry.proto\x12\x1chw.trezor.messages.telemetry\x1a\r\
- options.proto\"\x0e\n\x0cTelemetryGet\"n\n\tTelemetry\x12\x1c\n\nmin_tem\
- p_c\x18\x01\x20\x01(\x11R\x08minTempC\x12\x1c\n\nmax_temp_c\x18\x02\x20\
- \x01(\x11R\x08maxTempC\x12%\n\x0ebattery_errors\x18\x03\x20\x01(\rR\rbat\
- teryErrorsBA\n#com.satoshilabs.trezor.lib.protobufB\x16TrezorMessageTele\
- metry\x80\xa6\x1d\x01\
+ options.proto\"\x0e\n\x0cTelemetryGet\"\x95\x01\n\tTelemetry\x12\x1c\n\n\
+ min_temp_c\x18\x01\x20\x01(\x11R\x08minTempC\x12\x1c\n\nmax_temp_c\x18\
+ \x02\x20\x01(\x11R\x08maxTempC\x12%\n\x0ebattery_errors\x18\x03\x20\x01(\
+ \rR\rbatteryErrors\x12%\n\x0ebattery_cycles\x18\x04\x20\x01(\x11R\rbatte\
+ ryCyclesBA\n#com.satoshilabs.trezor.lib.protobufB\x16TrezorMessageTeleme\
+ try\x80\xa6\x1d\x01\
";
/// `FileDescriptorProto` object which was a source for this generated file
Why this scored 15/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.