diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index 168540025..fa3cf1311 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -37,6 +37,7 @@ except ImportError as e: have_test = False +import meshtastic.ota import meshtastic.util import meshtastic.serial_interface import meshtastic.tcp_interface @@ -60,7 +61,7 @@ have_powermon = False powermon_exception = e meter = None -from meshtastic.protobuf import channel_pb2, config_pb2, portnums_pb2, mesh_pb2 +from meshtastic.protobuf import admin_pb2, channel_pb2, config_pb2, portnums_pb2, mesh_pb2 from meshtastic.version import get_active_version logger = logging.getLogger(__name__) @@ -158,11 +159,11 @@ def _printSetting(config_type, uni_name, pref_value, repeated): config_values = getattr(config, config_type.name) if not wholeField: pref_value = getattr(config_values, pref.name) - repeated = pref.label == pref.LABEL_REPEATED + repeated = _is_repeated_field(pref) _printSetting(config_type, uni_name, pref_value, repeated) else: for field in config_values.ListFields(): - repeated = field[0].label == field[0].LABEL_REPEATED + repeated = _is_repeated_field(field[0]) _printSetting(config_type, field[0].name, field[1], repeated) else: # Always show whole field for remote node @@ -253,7 +254,7 @@ def setPref(config, comp_name, raw_val) -> bool: return False # repeating fields need to be handled with append, not setattr - if pref.label != pref.LABEL_REPEATED: + if not _is_repeated_field(pref): try: if config_type.message_type is not None: config_values = getattr(config_part, config_type.name) @@ -452,6 +453,41 @@ def onConnected(interface): waitForAckNak = True interface.getNode(args.dest, False, **getNode_kwargs).rebootOTA() + if args.ota_update: + closeNow = True + waitForAckNak = True + + if not isinstance(interface, meshtastic.tcp_interface.TCPInterface): + meshtastic.util.our_exit( + "Error: OTA update currently requires a TCP connection to the node (use --host)." + ) + + ota = meshtastic.ota.ESP32WiFiOTA(args.ota_update, interface.hostname) + + print(f"Triggering OTA update on {interface.hostname}...") + interface.getNode(args.dest, False, **getNode_kwargs).startOTA( + ota_mode=admin_pb2.OTAMode.OTA_WIFI, + ota_file_hash=ota.hash_bytes() + ) + + print("Waiting for device to reboot into OTA mode...") + time.sleep(5) + + retries = 5 + while retries > 0: + try: + ota.update() + break + + except Exception as e: + retries -= 1 + if retries == 0: + meshtastic.util.our_exit(f"\nOTA update failed: {e}") + + time.sleep(2) + + print("\nOTA update completed successfully!") + if args.enter_dfu: closeNow = True waitForAckNak = True @@ -1131,6 +1167,14 @@ def subscribe() -> None: # pub.subscribe(onNode, "meshtastic.node") +def _is_repeated_field(field_desc) -> bool: + """Return True if the protobuf field is repeated. + Protobuf 6.31.0 and later use an is_repeated property, while older versions compare against the label field. + """ + if hasattr(field_desc, "is_repeated"): + return bool(field_desc.is_repeated) + return field_desc.label == field_desc.LABEL_REPEATED + def set_missing_flags_false(config_dict: dict, true_defaults: set[tuple[str, str]]) -> None: """Ensure that missing default=True keys are present in the config_dict and set to False.""" for path in true_defaults: @@ -1904,10 +1948,18 @@ def addRemoteAdminArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentPars group.add_argument( "--reboot-ota", - help="Tell the destination node to reboot into factory firmware (ESP32)", + help="Tell the destination node to reboot into factory firmware (ESP32, firmware version <2.7.18)", action="store_true", ) + group.add_argument( + "--ota-update", + help="Perform an OTA update on the local node (ESP32, firmware version >=2.7.18, WiFi/TCP only for now). " + "Specify the path to the firmware file.", + metavar="FIRMWARE_FILE", + action="store", + ) + group.add_argument( "--enter-dfu", help="Tell the destination node to enter DFU mode (NRF52)", diff --git a/meshtastic/mesh_interface.py b/meshtastic/mesh_interface.py index 20e43b9d9..5dfb393b7 100644 --- a/meshtastic/mesh_interface.py +++ b/meshtastic/mesh_interface.py @@ -419,6 +419,7 @@ def sendText( channelIndex: int = 0, portNum: portnums_pb2.PortNum.ValueType = portnums_pb2.PortNum.TEXT_MESSAGE_APP, replyId: Optional[int]=None, + hopLimit: Optional[int]=None, ): """Send a utf8 string to some other node, if the node has a display it will also be shown on the device. @@ -436,6 +437,7 @@ def sendText( portNum -- the application portnum (similar to IP port numbers) of the destination, see portnums.proto for a list replyId -- the ID of the message that this packet is a response to + hopLimit {int} -- hop limit to use Returns the sent packet. The id field will be populated in this packet and can be used to track future message acks/naks. @@ -449,7 +451,8 @@ def sendText( wantResponse=wantResponse, onResponse=onResponse, channelIndex=channelIndex, - replyId=replyId + replyId=replyId, + hopLimit=hopLimit, ) @@ -459,6 +462,7 @@ def sendAlert( destinationId: Union[int, str] = BROADCAST_ADDR, onResponse: Optional[Callable[[dict], Any]] = None, channelIndex: int = 0, + hopLimit: Optional[int]=None, ): """Send an alert text to some other node. This is similar to a text message, but carries a higher priority and is capable of generating special notifications @@ -470,6 +474,7 @@ def sendAlert( Keyword Arguments: destinationId {nodeId or nodeNum} -- where to send this message (default: {BROADCAST_ADDR}) + hopLimit {int} -- hop limit to use Returns the sent packet. The id field will be populated in this packet and can be used to track future message acks/naks. @@ -483,7 +488,8 @@ def sendAlert( wantResponse=False, onResponse=onResponse, channelIndex=channelIndex, - priority=mesh_pb2.MeshPacket.Priority.ALERT + priority=mesh_pb2.MeshPacket.Priority.ALERT, + hopLimit=hopLimit, ) def sendMqttClientProxyMessage(self, topic: str, data: bytes): @@ -585,6 +591,7 @@ def sendPosition( wantAck: bool = False, wantResponse: bool = False, channelIndex: int = 0, + hopLimit: Optional[int]=None, ): """ Send a position packet to some other node (normally a broadcast) @@ -621,6 +628,7 @@ def sendPosition( wantResponse=wantResponse, onResponse=onResponse, channelIndex=channelIndex, + hopLimit=hopLimit, ) if wantResponse: self.waitForPosition() @@ -673,7 +681,8 @@ def sendTraceRoute( hopLimit=hopLimit, ) # extend timeout based on number of nodes, limit by configured hopLimit - waitFactor = min(len(self.nodes) - 1 if self.nodes else 0, hopLimit) + nodes_based_factor = (len(self.nodes) - 1) if self.nodes else (hopLimit + 1) + waitFactor = max(1, min(nodes_based_factor, hopLimit + 1)) self.waitForTraceRoute(waitFactor) def onResponseTraceRoute(self, p: dict): @@ -726,7 +735,8 @@ def sendTelemetry( destinationId: Union[int, str] = BROADCAST_ADDR, wantResponse: bool = False, channelIndex: int = 0, - telemetryType: str = "device_metrics" + telemetryType: str = "device_metrics", + hopLimit: Optional[int]=None, ): """Send telemetry and optionally ask for a response""" r = telemetry_pb2.Telemetry() @@ -773,6 +783,7 @@ def sendTelemetry( wantResponse=wantResponse, onResponse=onResponse, channelIndex=channelIndex, + hopLimit=hopLimit, ) if wantResponse: self.waitForTelemetry() @@ -842,6 +853,7 @@ def sendWaypoint( wantAck: bool = True, wantResponse: bool = False, channelIndex: int = 0, + hopLimit: Optional[int]=None, ): # pylint: disable=R0913 """ Send a waypoint packet to some other node (normally a broadcast) @@ -882,6 +894,7 @@ def sendWaypoint( wantResponse=wantResponse, onResponse=onResponse, channelIndex=channelIndex, + hopLimit=hopLimit, ) if wantResponse: self.waitForWaypoint() @@ -894,6 +907,7 @@ def deleteWaypoint( wantAck: bool = True, wantResponse: bool = False, channelIndex: int = 0, + hopLimit: Optional[int]=None, ): """ Send a waypoint deletion packet to some other node (normally a broadcast) @@ -920,6 +934,7 @@ def deleteWaypoint( wantResponse=wantResponse, onResponse=onResponse, channelIndex=channelIndex, + hopLimit=hopLimit, ) if wantResponse: self.waitForWaypoint() @@ -1455,6 +1470,10 @@ def _handleFromRadio(self, fromRadioBytes): self.localNode.moduleConfig.paxcounter.CopyFrom( fromRadio.moduleConfig.paxcounter ) + elif fromRadio.moduleConfig.HasField("traffic_management"): + self.localNode.moduleConfig.traffic_management.CopyFrom( + fromRadio.moduleConfig.traffic_management + ) else: logger.debug("Unexpected FromRadio payload") diff --git a/meshtastic/node.py b/meshtastic/node.py index afb5611ac..66b6312ff 100644 --- a/meshtastic/node.py +++ b/meshtastic/node.py @@ -170,11 +170,10 @@ def requestConfig(self, configType): p.get_config_request = configType else: - msgIndex = configType.index if configType.containing_type.name == "LocalConfig": - p.get_config_request = msgIndex + p.get_config_request = admin_pb2.AdminMessage.ConfigType.Value(configType.name.upper() + "_CONFIG") else: - p.get_module_config_request = msgIndex + p.get_module_config_request = configType.index self._sendAdmin(p, wantResponse=True, onResponse=onResponse) if onResponse: @@ -245,6 +244,8 @@ def writeConfig(self, config_name): p.set_module_config.ambient_lighting.CopyFrom(self.moduleConfig.ambient_lighting) elif config_name == "paxcounter": p.set_module_config.paxcounter.CopyFrom(self.moduleConfig.paxcounter) + elif config_name == "traffic_management": + p.set_module_config.traffic_management.CopyFrom(self.moduleConfig.traffic_management) else: our_exit(f"Error: No valid config with name {config_name}") @@ -654,7 +655,7 @@ def commitSettingsTransaction(self): return self._sendAdmin(p, onResponse=onResponse) def rebootOTA(self, secs: int = 10): - """Tell the node to reboot into factory firmware.""" + """Tell the node to reboot into factory firmware (firmware < 2.7.18).""" self.ensureSessionKey() p = admin_pb2.AdminMessage() p.reboot_ota_seconds = secs @@ -667,6 +668,22 @@ def rebootOTA(self, secs: int = 10): onResponse = self.onAckNak return self._sendAdmin(p, onResponse=onResponse) + def startOTA( + self, + ota_mode: admin_pb2.OTAMode.ValueType, + ota_file_hash: bytes, + ): + """Tell the node to start OTA mode (firmware >= 2.7.18).""" + if self != self.iface.localNode: + raise ValueError("startOTA only possible in local node") + + self.ensureSessionKey() + p = admin_pb2.AdminMessage() + p.ota_request.reboot_ota_mode = ota_mode + p.ota_request.ota_hash = ota_file_hash + + return self._sendAdmin(p) + def enterDFUMode(self): """Tell the node to enter DFU mode (NRF52).""" self.ensureSessionKey() diff --git a/meshtastic/ota.py b/meshtastic/ota.py new file mode 100644 index 000000000..af9feae42 --- /dev/null +++ b/meshtastic/ota.py @@ -0,0 +1,128 @@ +"""Meshtastic ESP32 Unified OTA +""" +import os +import hashlib +import socket +import logging +from typing import Optional, Callable + + +logger = logging.getLogger(__name__) + + +def _file_sha256(filename: str): + """Calculate SHA256 hash of a file.""" + sha256_hash = hashlib.sha256() + + with open(filename, "rb") as f: + for byte_block in iter(lambda: f.read(4096), b""): + sha256_hash.update(byte_block) + + return sha256_hash + + +class OTAError(Exception): + """Exception for OTA errors.""" + + +class ESP32WiFiOTA: + """ESP32 WiFi Unified OTA updates.""" + + def __init__(self, filename: str, hostname: str, port: int = 3232): + self._filename = filename + self._hostname = hostname + self._port = port + self._socket: Optional[socket.socket] = None + + if not os.path.exists(self._filename): + raise FileNotFoundError(f"File {self._filename} does not exist") + + self._file_hash = _file_sha256(self._filename) + + def _read_line(self) -> str: + """Read a line from the socket.""" + if not self._socket: + raise ConnectionError("Socket not connected") + + line = b"" + while not line.endswith(b"\n"): + char = self._socket.recv(1) + + if not char: + raise ConnectionError("Connection closed while waiting for response") + + line += char + + return line.decode("utf-8").strip() + + def hash_bytes(self) -> bytes: + """Return the hash as bytes.""" + return self._file_hash.digest() + + def hash_hex(self) -> str: + """Return the hash as a hex string.""" + return self._file_hash.hexdigest() + + def update(self, progress_callback: Optional[Callable[[int, int], None]] = None): + """Perform the OTA update.""" + with open(self._filename, "rb") as f: + data = f.read() + size = len(data) + + logger.info(f"Starting OTA update with {self._filename} ({size} bytes, hash {self.hash_hex()})") + + self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self._socket.settimeout(15) + try: + self._socket.connect((self._hostname, self._port)) + logger.debug(f"Connected to {self._hostname}:{self._port}") + + # Send start command + self._socket.sendall(f"OTA {size} {self.hash_hex()}\n".encode("utf-8")) + + # Wait for OK from the device + while True: + response = self._read_line() + if response == "OK": + break + + if response == "ERASING": + logger.info("Device is erasing flash...") + elif response.startswith("ERR "): + raise OTAError(f"Device reported error: {response}") + else: + logger.warning(f"Unexpected response: {response}") + + # Stream firmware + sent_bytes = 0 + chunk_size = 1024 + while sent_bytes < size: + chunk = data[sent_bytes : sent_bytes + chunk_size] + self._socket.sendall(chunk) + sent_bytes += len(chunk) + + if progress_callback: + progress_callback(sent_bytes, size) + else: + print(f"[{sent_bytes / size * 100:5.1f}%] Sent {sent_bytes} of {size} bytes...", end="\r") + + if not progress_callback: + print() + + # Wait for OK from device + logger.info("Firmware sent, waiting for verification...") + while True: + response = self._read_line() + if response == "OK": + logger.info("OTA update completed successfully!") + break + + if response.startswith("ERR "): + raise OTAError(f"OTA update failed: {response}") + elif response != "ACK": + logger.warning(f"Unexpected final response: {response}") + + finally: + if self._socket: + self._socket.close() + self._socket = None diff --git a/meshtastic/protobuf/admin_pb2.py b/meshtastic/protobuf/admin_pb2.py index 1d93304e1..d76061c1f 100644 --- a/meshtastic/protobuf/admin_pb2.py +++ b/meshtastic/protobuf/admin_pb2.py @@ -19,7 +19,7 @@ from meshtastic.protobuf import module_config_pb2 as meshtastic_dot_protobuf_dot_module__config__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1fmeshtastic/protobuf/admin.proto\x12\x13meshtastic.protobuf\x1a!meshtastic/protobuf/channel.proto\x1a meshtastic/protobuf/config.proto\x1a+meshtastic/protobuf/connection_status.proto\x1a#meshtastic/protobuf/device_ui.proto\x1a\x1emeshtastic/protobuf/mesh.proto\x1a\'meshtastic/protobuf/module_config.proto\"\xd2\x1c\n\x0c\x41\x64minMessage\x12\x17\n\x0fsession_passkey\x18\x65 \x01(\x0c\x12\x1d\n\x13get_channel_request\x18\x01 \x01(\rH\x00\x12<\n\x14get_channel_response\x18\x02 \x01(\x0b\x32\x1c.meshtastic.protobuf.ChannelH\x00\x12\x1b\n\x11get_owner_request\x18\x03 \x01(\x08H\x00\x12\x37\n\x12get_owner_response\x18\x04 \x01(\x0b\x32\x19.meshtastic.protobuf.UserH\x00\x12J\n\x12get_config_request\x18\x05 \x01(\x0e\x32,.meshtastic.protobuf.AdminMessage.ConfigTypeH\x00\x12:\n\x13get_config_response\x18\x06 \x01(\x0b\x32\x1b.meshtastic.protobuf.ConfigH\x00\x12W\n\x19get_module_config_request\x18\x07 \x01(\x0e\x32\x32.meshtastic.protobuf.AdminMessage.ModuleConfigTypeH\x00\x12G\n\x1aget_module_config_response\x18\x08 \x01(\x0b\x32!.meshtastic.protobuf.ModuleConfigH\x00\x12\x34\n*get_canned_message_module_messages_request\x18\n \x01(\x08H\x00\x12\x35\n+get_canned_message_module_messages_response\x18\x0b \x01(\tH\x00\x12%\n\x1bget_device_metadata_request\x18\x0c \x01(\x08H\x00\x12K\n\x1cget_device_metadata_response\x18\r \x01(\x0b\x32#.meshtastic.protobuf.DeviceMetadataH\x00\x12\x1e\n\x14get_ringtone_request\x18\x0e \x01(\x08H\x00\x12\x1f\n\x15get_ringtone_response\x18\x0f \x01(\tH\x00\x12.\n$get_device_connection_status_request\x18\x10 \x01(\x08H\x00\x12\\\n%get_device_connection_status_response\x18\x11 \x01(\x0b\x32+.meshtastic.protobuf.DeviceConnectionStatusH\x00\x12:\n\x0cset_ham_mode\x18\x12 \x01(\x0b\x32\".meshtastic.protobuf.HamParametersH\x00\x12/\n%get_node_remote_hardware_pins_request\x18\x13 \x01(\x08H\x00\x12\x65\n&get_node_remote_hardware_pins_response\x18\x14 \x01(\x0b\x32\x33.meshtastic.protobuf.NodeRemoteHardwarePinsResponseH\x00\x12 \n\x16\x65nter_dfu_mode_request\x18\x15 \x01(\x08H\x00\x12\x1d\n\x13\x64\x65lete_file_request\x18\x16 \x01(\tH\x00\x12\x13\n\tset_scale\x18\x17 \x01(\rH\x00\x12N\n\x12\x62\x61\x63kup_preferences\x18\x18 \x01(\x0e\x32\x30.meshtastic.protobuf.AdminMessage.BackupLocationH\x00\x12O\n\x13restore_preferences\x18\x19 \x01(\x0e\x32\x30.meshtastic.protobuf.AdminMessage.BackupLocationH\x00\x12U\n\x19remove_backup_preferences\x18\x1a \x01(\x0e\x32\x30.meshtastic.protobuf.AdminMessage.BackupLocationH\x00\x12H\n\x10send_input_event\x18\x1b \x01(\x0b\x32,.meshtastic.protobuf.AdminMessage.InputEventH\x00\x12.\n\tset_owner\x18 \x01(\x0b\x32\x19.meshtastic.protobuf.UserH\x00\x12\x33\n\x0bset_channel\x18! \x01(\x0b\x32\x1c.meshtastic.protobuf.ChannelH\x00\x12\x31\n\nset_config\x18\" \x01(\x0b\x32\x1b.meshtastic.protobuf.ConfigH\x00\x12>\n\x11set_module_config\x18# \x01(\x0b\x32!.meshtastic.protobuf.ModuleConfigH\x00\x12,\n\"set_canned_message_module_messages\x18$ \x01(\tH\x00\x12\x1e\n\x14set_ringtone_message\x18% \x01(\tH\x00\x12\x1b\n\x11remove_by_nodenum\x18& \x01(\rH\x00\x12\x1b\n\x11set_favorite_node\x18\' \x01(\rH\x00\x12\x1e\n\x14remove_favorite_node\x18( \x01(\rH\x00\x12;\n\x12set_fixed_position\x18) \x01(\x0b\x32\x1d.meshtastic.protobuf.PositionH\x00\x12\x1f\n\x15remove_fixed_position\x18* \x01(\x08H\x00\x12\x17\n\rset_time_only\x18+ \x01(\x07H\x00\x12\x1f\n\x15get_ui_config_request\x18, \x01(\x08H\x00\x12\x45\n\x16get_ui_config_response\x18- \x01(\x0b\x32#.meshtastic.protobuf.DeviceUIConfigH\x00\x12>\n\x0fstore_ui_config\x18. \x01(\x0b\x32#.meshtastic.protobuf.DeviceUIConfigH\x00\x12\x1a\n\x10set_ignored_node\x18/ \x01(\rH\x00\x12\x1d\n\x13remove_ignored_node\x18\x30 \x01(\rH\x00\x12\x1b\n\x11toggle_muted_node\x18\x31 \x01(\rH\x00\x12\x1d\n\x13\x62\x65gin_edit_settings\x18@ \x01(\x08H\x00\x12\x1e\n\x14\x63ommit_edit_settings\x18\x41 \x01(\x08H\x00\x12\x39\n\x0b\x61\x64\x64_contact\x18\x42 \x01(\x0b\x32\".meshtastic.protobuf.SharedContactH\x00\x12\x45\n\x10key_verification\x18\x43 \x01(\x0b\x32).meshtastic.protobuf.KeyVerificationAdminH\x00\x12\x1e\n\x14\x66\x61\x63tory_reset_device\x18^ \x01(\x05H\x00\x12 \n\x12reboot_ota_seconds\x18_ \x01(\x05\x42\x02\x18\x01H\x00\x12\x18\n\x0e\x65xit_simulator\x18` \x01(\x08H\x00\x12\x18\n\x0ereboot_seconds\x18\x61 \x01(\x05H\x00\x12\x1a\n\x10shutdown_seconds\x18\x62 \x01(\x05H\x00\x12\x1e\n\x14\x66\x61\x63tory_reset_config\x18\x63 \x01(\x05H\x00\x12\x16\n\x0cnodedb_reset\x18\x64 \x01(\x08H\x00\x12\x41\n\x0bota_request\x18\x66 \x01(\x0b\x32*.meshtastic.protobuf.AdminMessage.OTAEventH\x00\x12:\n\rsensor_config\x18g \x01(\x0b\x32!.meshtastic.protobuf.SensorConfigH\x00\x1aS\n\nInputEvent\x12\x12\n\nevent_code\x18\x01 \x01(\r\x12\x0f\n\x07kb_char\x18\x02 \x01(\r\x12\x0f\n\x07touch_x\x18\x03 \x01(\r\x12\x0f\n\x07touch_y\x18\x04 \x01(\r\x1aS\n\x08OTAEvent\x12\x35\n\x0freboot_ota_mode\x18\x01 \x01(\x0e\x32\x1c.meshtastic.protobuf.OTAMode\x12\x10\n\x08ota_hash\x18\x02 \x01(\x0c\"\xd6\x01\n\nConfigType\x12\x11\n\rDEVICE_CONFIG\x10\x00\x12\x13\n\x0fPOSITION_CONFIG\x10\x01\x12\x10\n\x0cPOWER_CONFIG\x10\x02\x12\x12\n\x0eNETWORK_CONFIG\x10\x03\x12\x12\n\x0e\x44ISPLAY_CONFIG\x10\x04\x12\x0f\n\x0bLORA_CONFIG\x10\x05\x12\x14\n\x10\x42LUETOOTH_CONFIG\x10\x06\x12\x13\n\x0fSECURITY_CONFIG\x10\x07\x12\x15\n\x11SESSIONKEY_CONFIG\x10\x08\x12\x13\n\x0f\x44\x45VICEUI_CONFIG\x10\t\"\xf3\x02\n\x10ModuleConfigType\x12\x0f\n\x0bMQTT_CONFIG\x10\x00\x12\x11\n\rSERIAL_CONFIG\x10\x01\x12\x13\n\x0f\x45XTNOTIF_CONFIG\x10\x02\x12\x17\n\x13STOREFORWARD_CONFIG\x10\x03\x12\x14\n\x10RANGETEST_CONFIG\x10\x04\x12\x14\n\x10TELEMETRY_CONFIG\x10\x05\x12\x14\n\x10\x43\x41NNEDMSG_CONFIG\x10\x06\x12\x10\n\x0c\x41UDIO_CONFIG\x10\x07\x12\x19\n\x15REMOTEHARDWARE_CONFIG\x10\x08\x12\x17\n\x13NEIGHBORINFO_CONFIG\x10\t\x12\x1a\n\x16\x41MBIENTLIGHTING_CONFIG\x10\n\x12\x1a\n\x16\x44\x45TECTIONSENSOR_CONFIG\x10\x0b\x12\x15\n\x11PAXCOUNTER_CONFIG\x10\x0c\x12\x18\n\x14STATUSMESSAGE_CONFIG\x10\r\x12\x1c\n\x18TRAFFICMANAGEMENT_CONFIG\x10\x0e\"#\n\x0e\x42\x61\x63kupLocation\x12\t\n\x05\x46LASH\x10\x00\x12\x06\n\x02SD\x10\x01\x42\x11\n\x0fpayload_variant\"[\n\rHamParameters\x12\x11\n\tcall_sign\x18\x01 \x01(\t\x12\x10\n\x08tx_power\x18\x02 \x01(\x05\x12\x11\n\tfrequency\x18\x03 \x01(\x02\x12\x12\n\nshort_name\x18\x04 \x01(\t\"o\n\x1eNodeRemoteHardwarePinsResponse\x12M\n\x19node_remote_hardware_pins\x18\x01 \x03(\x0b\x32*.meshtastic.protobuf.NodeRemoteHardwarePin\"|\n\rSharedContact\x12\x10\n\x08node_num\x18\x01 \x01(\r\x12\'\n\x04user\x18\x02 \x01(\x0b\x32\x19.meshtastic.protobuf.User\x12\x15\n\rshould_ignore\x18\x03 \x01(\x08\x12\x19\n\x11manually_verified\x18\x04 \x01(\x08\"\xa5\x02\n\x14KeyVerificationAdmin\x12K\n\x0cmessage_type\x18\x01 \x01(\x0e\x32\x35.meshtastic.protobuf.KeyVerificationAdmin.MessageType\x12\x16\n\x0eremote_nodenum\x18\x02 \x01(\r\x12\r\n\x05nonce\x18\x03 \x01(\x04\x12\x1c\n\x0fsecurity_number\x18\x04 \x01(\rH\x00\x88\x01\x01\"g\n\x0bMessageType\x12\x19\n\x15INITIATE_VERIFICATION\x10\x00\x12\x1b\n\x17PROVIDE_SECURITY_NUMBER\x10\x01\x12\r\n\tDO_VERIFY\x10\x02\x12\x11\n\rDO_NOT_VERIFY\x10\x03\x42\x12\n\x10_security_number\"\xb9\x01\n\x0cSensorConfig\x12\x37\n\x0cscd4x_config\x18\x01 \x01(\x0b\x32!.meshtastic.protobuf.SCD4X_config\x12\x37\n\x0csen5x_config\x18\x02 \x01(\x0b\x32!.meshtastic.protobuf.SEN5X_config\x12\x37\n\x0cscd30_config\x18\x03 \x01(\x0b\x32!.meshtastic.protobuf.SCD30_config\"\xe2\x02\n\x0cSCD4X_config\x12\x14\n\x07set_asc\x18\x01 \x01(\x08H\x00\x88\x01\x01\x12 \n\x13set_target_co2_conc\x18\x02 \x01(\rH\x01\x88\x01\x01\x12\x1c\n\x0fset_temperature\x18\x03 \x01(\x02H\x02\x88\x01\x01\x12\x19\n\x0cset_altitude\x18\x04 \x01(\rH\x03\x88\x01\x01\x12!\n\x14set_ambient_pressure\x18\x05 \x01(\rH\x04\x88\x01\x01\x12\x1a\n\rfactory_reset\x18\x06 \x01(\x08H\x05\x88\x01\x01\x12\x1b\n\x0eset_power_mode\x18\x07 \x01(\x08H\x06\x88\x01\x01\x42\n\n\x08_set_ascB\x16\n\x14_set_target_co2_concB\x12\n\x10_set_temperatureB\x0f\n\r_set_altitudeB\x17\n\x15_set_ambient_pressureB\x10\n\x0e_factory_resetB\x11\n\x0f_set_power_mode\"v\n\x0cSEN5X_config\x12\x1c\n\x0fset_temperature\x18\x01 \x01(\x02H\x00\x88\x01\x01\x12\x1e\n\x11set_one_shot_mode\x18\x02 \x01(\x08H\x01\x88\x01\x01\x42\x12\n\x10_set_temperatureB\x14\n\x12_set_one_shot_mode\"\xb4\x02\n\x0cSCD30_config\x12\x14\n\x07set_asc\x18\x01 \x01(\x08H\x00\x88\x01\x01\x12 \n\x13set_target_co2_conc\x18\x02 \x01(\rH\x01\x88\x01\x01\x12\x1c\n\x0fset_temperature\x18\x03 \x01(\x02H\x02\x88\x01\x01\x12\x19\n\x0cset_altitude\x18\x04 \x01(\rH\x03\x88\x01\x01\x12%\n\x18set_measurement_interval\x18\x05 \x01(\rH\x04\x88\x01\x01\x12\x17\n\nsoft_reset\x18\x06 \x01(\x08H\x05\x88\x01\x01\x42\n\n\x08_set_ascB\x16\n\x14_set_target_co2_concB\x12\n\x10_set_temperatureB\x0f\n\r_set_altitudeB\x1b\n\x19_set_measurement_intervalB\r\n\x0b_soft_reset*7\n\x07OTAMode\x12\x11\n\rNO_REBOOT_OTA\x10\x00\x12\x0b\n\x07OTA_BLE\x10\x01\x12\x0c\n\x08OTA_WIFI\x10\x02\x42\x61\n\x14org.meshtastic.protoB\x0b\x41\x64minProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1fmeshtastic/protobuf/admin.proto\x12\x13meshtastic.protobuf\x1a!meshtastic/protobuf/channel.proto\x1a meshtastic/protobuf/config.proto\x1a+meshtastic/protobuf/connection_status.proto\x1a#meshtastic/protobuf/device_ui.proto\x1a\x1emeshtastic/protobuf/mesh.proto\x1a\'meshtastic/protobuf/module_config.proto\"\xd2\x1c\n\x0c\x41\x64minMessage\x12\x17\n\x0fsession_passkey\x18\x65 \x01(\x0c\x12\x1d\n\x13get_channel_request\x18\x01 \x01(\rH\x00\x12<\n\x14get_channel_response\x18\x02 \x01(\x0b\x32\x1c.meshtastic.protobuf.ChannelH\x00\x12\x1b\n\x11get_owner_request\x18\x03 \x01(\x08H\x00\x12\x37\n\x12get_owner_response\x18\x04 \x01(\x0b\x32\x19.meshtastic.protobuf.UserH\x00\x12J\n\x12get_config_request\x18\x05 \x01(\x0e\x32,.meshtastic.protobuf.AdminMessage.ConfigTypeH\x00\x12:\n\x13get_config_response\x18\x06 \x01(\x0b\x32\x1b.meshtastic.protobuf.ConfigH\x00\x12W\n\x19get_module_config_request\x18\x07 \x01(\x0e\x32\x32.meshtastic.protobuf.AdminMessage.ModuleConfigTypeH\x00\x12G\n\x1aget_module_config_response\x18\x08 \x01(\x0b\x32!.meshtastic.protobuf.ModuleConfigH\x00\x12\x34\n*get_canned_message_module_messages_request\x18\n \x01(\x08H\x00\x12\x35\n+get_canned_message_module_messages_response\x18\x0b \x01(\tH\x00\x12%\n\x1bget_device_metadata_request\x18\x0c \x01(\x08H\x00\x12K\n\x1cget_device_metadata_response\x18\r \x01(\x0b\x32#.meshtastic.protobuf.DeviceMetadataH\x00\x12\x1e\n\x14get_ringtone_request\x18\x0e \x01(\x08H\x00\x12\x1f\n\x15get_ringtone_response\x18\x0f \x01(\tH\x00\x12.\n$get_device_connection_status_request\x18\x10 \x01(\x08H\x00\x12\\\n%get_device_connection_status_response\x18\x11 \x01(\x0b\x32+.meshtastic.protobuf.DeviceConnectionStatusH\x00\x12:\n\x0cset_ham_mode\x18\x12 \x01(\x0b\x32\".meshtastic.protobuf.HamParametersH\x00\x12/\n%get_node_remote_hardware_pins_request\x18\x13 \x01(\x08H\x00\x12\x65\n&get_node_remote_hardware_pins_response\x18\x14 \x01(\x0b\x32\x33.meshtastic.protobuf.NodeRemoteHardwarePinsResponseH\x00\x12 \n\x16\x65nter_dfu_mode_request\x18\x15 \x01(\x08H\x00\x12\x1d\n\x13\x64\x65lete_file_request\x18\x16 \x01(\tH\x00\x12\x13\n\tset_scale\x18\x17 \x01(\rH\x00\x12N\n\x12\x62\x61\x63kup_preferences\x18\x18 \x01(\x0e\x32\x30.meshtastic.protobuf.AdminMessage.BackupLocationH\x00\x12O\n\x13restore_preferences\x18\x19 \x01(\x0e\x32\x30.meshtastic.protobuf.AdminMessage.BackupLocationH\x00\x12U\n\x19remove_backup_preferences\x18\x1a \x01(\x0e\x32\x30.meshtastic.protobuf.AdminMessage.BackupLocationH\x00\x12H\n\x10send_input_event\x18\x1b \x01(\x0b\x32,.meshtastic.protobuf.AdminMessage.InputEventH\x00\x12.\n\tset_owner\x18 \x01(\x0b\x32\x19.meshtastic.protobuf.UserH\x00\x12\x33\n\x0bset_channel\x18! \x01(\x0b\x32\x1c.meshtastic.protobuf.ChannelH\x00\x12\x31\n\nset_config\x18\" \x01(\x0b\x32\x1b.meshtastic.protobuf.ConfigH\x00\x12>\n\x11set_module_config\x18# \x01(\x0b\x32!.meshtastic.protobuf.ModuleConfigH\x00\x12,\n\"set_canned_message_module_messages\x18$ \x01(\tH\x00\x12\x1e\n\x14set_ringtone_message\x18% \x01(\tH\x00\x12\x1b\n\x11remove_by_nodenum\x18& \x01(\rH\x00\x12\x1b\n\x11set_favorite_node\x18\' \x01(\rH\x00\x12\x1e\n\x14remove_favorite_node\x18( \x01(\rH\x00\x12;\n\x12set_fixed_position\x18) \x01(\x0b\x32\x1d.meshtastic.protobuf.PositionH\x00\x12\x1f\n\x15remove_fixed_position\x18* \x01(\x08H\x00\x12\x17\n\rset_time_only\x18+ \x01(\x07H\x00\x12\x1f\n\x15get_ui_config_request\x18, \x01(\x08H\x00\x12\x45\n\x16get_ui_config_response\x18- \x01(\x0b\x32#.meshtastic.protobuf.DeviceUIConfigH\x00\x12>\n\x0fstore_ui_config\x18. \x01(\x0b\x32#.meshtastic.protobuf.DeviceUIConfigH\x00\x12\x1a\n\x10set_ignored_node\x18/ \x01(\rH\x00\x12\x1d\n\x13remove_ignored_node\x18\x30 \x01(\rH\x00\x12\x1b\n\x11toggle_muted_node\x18\x31 \x01(\rH\x00\x12\x1d\n\x13\x62\x65gin_edit_settings\x18@ \x01(\x08H\x00\x12\x1e\n\x14\x63ommit_edit_settings\x18\x41 \x01(\x08H\x00\x12\x39\n\x0b\x61\x64\x64_contact\x18\x42 \x01(\x0b\x32\".meshtastic.protobuf.SharedContactH\x00\x12\x45\n\x10key_verification\x18\x43 \x01(\x0b\x32).meshtastic.protobuf.KeyVerificationAdminH\x00\x12\x1e\n\x14\x66\x61\x63tory_reset_device\x18^ \x01(\x05H\x00\x12 \n\x12reboot_ota_seconds\x18_ \x01(\x05\x42\x02\x18\x01H\x00\x12\x18\n\x0e\x65xit_simulator\x18` \x01(\x08H\x00\x12\x18\n\x0ereboot_seconds\x18\x61 \x01(\x05H\x00\x12\x1a\n\x10shutdown_seconds\x18\x62 \x01(\x05H\x00\x12\x1e\n\x14\x66\x61\x63tory_reset_config\x18\x63 \x01(\x05H\x00\x12\x16\n\x0cnodedb_reset\x18\x64 \x01(\x08H\x00\x12\x41\n\x0bota_request\x18\x66 \x01(\x0b\x32*.meshtastic.protobuf.AdminMessage.OTAEventH\x00\x12:\n\rsensor_config\x18g \x01(\x0b\x32!.meshtastic.protobuf.SensorConfigH\x00\x1aS\n\nInputEvent\x12\x12\n\nevent_code\x18\x01 \x01(\r\x12\x0f\n\x07kb_char\x18\x02 \x01(\r\x12\x0f\n\x07touch_x\x18\x03 \x01(\r\x12\x0f\n\x07touch_y\x18\x04 \x01(\r\x1aS\n\x08OTAEvent\x12\x35\n\x0freboot_ota_mode\x18\x01 \x01(\x0e\x32\x1c.meshtastic.protobuf.OTAMode\x12\x10\n\x08ota_hash\x18\x02 \x01(\x0c\"\xd6\x01\n\nConfigType\x12\x11\n\rDEVICE_CONFIG\x10\x00\x12\x13\n\x0fPOSITION_CONFIG\x10\x01\x12\x10\n\x0cPOWER_CONFIG\x10\x02\x12\x12\n\x0eNETWORK_CONFIG\x10\x03\x12\x12\n\x0e\x44ISPLAY_CONFIG\x10\x04\x12\x0f\n\x0bLORA_CONFIG\x10\x05\x12\x14\n\x10\x42LUETOOTH_CONFIG\x10\x06\x12\x13\n\x0fSECURITY_CONFIG\x10\x07\x12\x15\n\x11SESSIONKEY_CONFIG\x10\x08\x12\x13\n\x0f\x44\x45VICEUI_CONFIG\x10\t\"\xf3\x02\n\x10ModuleConfigType\x12\x0f\n\x0bMQTT_CONFIG\x10\x00\x12\x11\n\rSERIAL_CONFIG\x10\x01\x12\x13\n\x0f\x45XTNOTIF_CONFIG\x10\x02\x12\x17\n\x13STOREFORWARD_CONFIG\x10\x03\x12\x14\n\x10RANGETEST_CONFIG\x10\x04\x12\x14\n\x10TELEMETRY_CONFIG\x10\x05\x12\x14\n\x10\x43\x41NNEDMSG_CONFIG\x10\x06\x12\x10\n\x0c\x41UDIO_CONFIG\x10\x07\x12\x19\n\x15REMOTEHARDWARE_CONFIG\x10\x08\x12\x17\n\x13NEIGHBORINFO_CONFIG\x10\t\x12\x1a\n\x16\x41MBIENTLIGHTING_CONFIG\x10\n\x12\x1a\n\x16\x44\x45TECTIONSENSOR_CONFIG\x10\x0b\x12\x15\n\x11PAXCOUNTER_CONFIG\x10\x0c\x12\x18\n\x14STATUSMESSAGE_CONFIG\x10\r\x12\x1c\n\x18TRAFFICMANAGEMENT_CONFIG\x10\x0e\"#\n\x0e\x42\x61\x63kupLocation\x12\t\n\x05\x46LASH\x10\x00\x12\x06\n\x02SD\x10\x01\x42\x11\n\x0fpayload_variant\"[\n\rHamParameters\x12\x11\n\tcall_sign\x18\x01 \x01(\t\x12\x10\n\x08tx_power\x18\x02 \x01(\x05\x12\x11\n\tfrequency\x18\x03 \x01(\x02\x12\x12\n\nshort_name\x18\x04 \x01(\t\"o\n\x1eNodeRemoteHardwarePinsResponse\x12M\n\x19node_remote_hardware_pins\x18\x01 \x03(\x0b\x32*.meshtastic.protobuf.NodeRemoteHardwarePin\"|\n\rSharedContact\x12\x10\n\x08node_num\x18\x01 \x01(\r\x12\'\n\x04user\x18\x02 \x01(\x0b\x32\x19.meshtastic.protobuf.User\x12\x15\n\rshould_ignore\x18\x03 \x01(\x08\x12\x19\n\x11manually_verified\x18\x04 \x01(\x08\"\xa5\x02\n\x14KeyVerificationAdmin\x12K\n\x0cmessage_type\x18\x01 \x01(\x0e\x32\x35.meshtastic.protobuf.KeyVerificationAdmin.MessageType\x12\x16\n\x0eremote_nodenum\x18\x02 \x01(\r\x12\r\n\x05nonce\x18\x03 \x01(\x04\x12\x1c\n\x0fsecurity_number\x18\x04 \x01(\rH\x00\x88\x01\x01\"g\n\x0bMessageType\x12\x19\n\x15INITIATE_VERIFICATION\x10\x00\x12\x1b\n\x17PROVIDE_SECURITY_NUMBER\x10\x01\x12\r\n\tDO_VERIFY\x10\x02\x12\x11\n\rDO_NOT_VERIFY\x10\x03\x42\x12\n\x10_security_number\"\x80\x01\n\x0cSensorConfig\x12\x37\n\x0cscd4x_config\x18\x01 \x01(\x0b\x32!.meshtastic.protobuf.SCD4X_config\x12\x37\n\x0csen5x_config\x18\x02 \x01(\x0b\x32!.meshtastic.protobuf.SEN5X_config\"\xe2\x02\n\x0cSCD4X_config\x12\x14\n\x07set_asc\x18\x01 \x01(\x08H\x00\x88\x01\x01\x12 \n\x13set_target_co2_conc\x18\x02 \x01(\rH\x01\x88\x01\x01\x12\x1c\n\x0fset_temperature\x18\x03 \x01(\x02H\x02\x88\x01\x01\x12\x19\n\x0cset_altitude\x18\x04 \x01(\rH\x03\x88\x01\x01\x12!\n\x14set_ambient_pressure\x18\x05 \x01(\rH\x04\x88\x01\x01\x12\x1a\n\rfactory_reset\x18\x06 \x01(\x08H\x05\x88\x01\x01\x12\x1b\n\x0eset_power_mode\x18\x07 \x01(\x08H\x06\x88\x01\x01\x42\n\n\x08_set_ascB\x16\n\x14_set_target_co2_concB\x12\n\x10_set_temperatureB\x0f\n\r_set_altitudeB\x17\n\x15_set_ambient_pressureB\x10\n\x0e_factory_resetB\x11\n\x0f_set_power_mode\"v\n\x0cSEN5X_config\x12\x1c\n\x0fset_temperature\x18\x01 \x01(\x02H\x00\x88\x01\x01\x12\x1e\n\x11set_one_shot_mode\x18\x02 \x01(\x08H\x01\x88\x01\x01\x42\x12\n\x10_set_temperatureB\x14\n\x12_set_one_shot_mode*7\n\x07OTAMode\x12\x11\n\rNO_REBOOT_OTA\x10\x00\x12\x0b\n\x07OTA_BLE\x10\x01\x12\x0c\n\x08OTA_WIFI\x10\x02\x42\x61\n\x14org.meshtastic.protoB\x0b\x41\x64minProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -29,8 +29,8 @@ DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\013AdminProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000' _ADMINMESSAGE.fields_by_name['reboot_ota_seconds']._options = None _ADMINMESSAGE.fields_by_name['reboot_ota_seconds']._serialized_options = b'\030\001' - _globals['_OTAMODE']._serialized_start=5553 - _globals['_OTAMODE']._serialized_end=5608 + _globals['_OTAMODE']._serialized_start=5185 + _globals['_OTAMODE']._serialized_end=5240 _globals['_ADMINMESSAGE']._serialized_start=281 _globals['_ADMINMESSAGE']._serialized_end=3947 _globals['_ADMINMESSAGE_INPUTEVENT']._serialized_start=3132 @@ -54,11 +54,9 @@ _globals['_KEYVERIFICATIONADMIN_MESSAGETYPE']._serialized_start=4452 _globals['_KEYVERIFICATIONADMIN_MESSAGETYPE']._serialized_end=4555 _globals['_SENSORCONFIG']._serialized_start=4578 - _globals['_SENSORCONFIG']._serialized_end=4763 - _globals['_SCD4X_CONFIG']._serialized_start=4766 - _globals['_SCD4X_CONFIG']._serialized_end=5120 - _globals['_SEN5X_CONFIG']._serialized_start=5122 - _globals['_SEN5X_CONFIG']._serialized_end=5240 - _globals['_SCD30_CONFIG']._serialized_start=5243 - _globals['_SCD30_CONFIG']._serialized_end=5551 + _globals['_SENSORCONFIG']._serialized_end=4706 + _globals['_SCD4X_CONFIG']._serialized_start=4709 + _globals['_SCD4X_CONFIG']._serialized_end=5063 + _globals['_SEN5X_CONFIG']._serialized_start=5065 + _globals['_SEN5X_CONFIG']._serialized_end=5183 # @@protoc_insertion_point(module_scope) diff --git a/meshtastic/protobuf/admin_pb2.pyi b/meshtastic/protobuf/admin_pb2.pyi index ccd672ddb..118ef86c7 100644 --- a/meshtastic/protobuf/admin_pb2.pyi +++ b/meshtastic/protobuf/admin_pb2.pyi @@ -1000,7 +1000,6 @@ class SensorConfig(google.protobuf.message.Message): SCD4X_CONFIG_FIELD_NUMBER: builtins.int SEN5X_CONFIG_FIELD_NUMBER: builtins.int - SCD30_CONFIG_FIELD_NUMBER: builtins.int @property def scd4x_config(self) -> global___SCD4X_config: """ @@ -1013,21 +1012,14 @@ class SensorConfig(google.protobuf.message.Message): SEN5X PM Sensor configuration """ - @property - def scd30_config(self) -> global___SCD30_config: - """ - SCD30 CO2 Sensor configuration - """ - def __init__( self, *, scd4x_config: global___SCD4X_config | None = ..., sen5x_config: global___SEN5X_config | None = ..., - scd30_config: global___SCD30_config | None = ..., ) -> None: ... - def HasField(self, field_name: typing.Literal["scd30_config", b"scd30_config", "scd4x_config", b"scd4x_config", "sen5x_config", b"sen5x_config"]) -> builtins.bool: ... - def ClearField(self, field_name: typing.Literal["scd30_config", b"scd30_config", "scd4x_config", b"scd4x_config", "sen5x_config", b"sen5x_config"]) -> None: ... + def HasField(self, field_name: typing.Literal["scd4x_config", b"scd4x_config", "sen5x_config", b"sen5x_config"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["scd4x_config", b"scd4x_config", "sen5x_config", b"sen5x_config"]) -> None: ... global___SensorConfig = SensorConfig @@ -1128,64 +1120,3 @@ class SEN5X_config(google.protobuf.message.Message): def WhichOneof(self, oneof_group: typing.Literal["_set_temperature", b"_set_temperature"]) -> typing.Literal["set_temperature"] | None: ... global___SEN5X_config = SEN5X_config - -@typing.final -class SCD30_config(google.protobuf.message.Message): - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - SET_ASC_FIELD_NUMBER: builtins.int - SET_TARGET_CO2_CONC_FIELD_NUMBER: builtins.int - SET_TEMPERATURE_FIELD_NUMBER: builtins.int - SET_ALTITUDE_FIELD_NUMBER: builtins.int - SET_MEASUREMENT_INTERVAL_FIELD_NUMBER: builtins.int - SOFT_RESET_FIELD_NUMBER: builtins.int - set_asc: builtins.bool - """ - Set Automatic self-calibration enabled - """ - set_target_co2_conc: builtins.int - """ - Recalibration target CO2 concentration in ppm (FRC or ASC) - """ - set_temperature: builtins.float - """ - Reference temperature in degC - """ - set_altitude: builtins.int - """ - Altitude of sensor in meters above sea level. 0 - 3000m (overrides ambient pressure) - """ - set_measurement_interval: builtins.int - """ - Power mode for sensor (true for low power, false for normal) - """ - soft_reset: builtins.bool - """ - Perform a factory reset of the sensor - """ - def __init__( - self, - *, - set_asc: builtins.bool | None = ..., - set_target_co2_conc: builtins.int | None = ..., - set_temperature: builtins.float | None = ..., - set_altitude: builtins.int | None = ..., - set_measurement_interval: builtins.int | None = ..., - soft_reset: builtins.bool | None = ..., - ) -> None: ... - def HasField(self, field_name: typing.Literal["_set_altitude", b"_set_altitude", "_set_asc", b"_set_asc", "_set_measurement_interval", b"_set_measurement_interval", "_set_target_co2_conc", b"_set_target_co2_conc", "_set_temperature", b"_set_temperature", "_soft_reset", b"_soft_reset", "set_altitude", b"set_altitude", "set_asc", b"set_asc", "set_measurement_interval", b"set_measurement_interval", "set_target_co2_conc", b"set_target_co2_conc", "set_temperature", b"set_temperature", "soft_reset", b"soft_reset"]) -> builtins.bool: ... - def ClearField(self, field_name: typing.Literal["_set_altitude", b"_set_altitude", "_set_asc", b"_set_asc", "_set_measurement_interval", b"_set_measurement_interval", "_set_target_co2_conc", b"_set_target_co2_conc", "_set_temperature", b"_set_temperature", "_soft_reset", b"_soft_reset", "set_altitude", b"set_altitude", "set_asc", b"set_asc", "set_measurement_interval", b"set_measurement_interval", "set_target_co2_conc", b"set_target_co2_conc", "set_temperature", b"set_temperature", "soft_reset", b"soft_reset"]) -> None: ... - @typing.overload - def WhichOneof(self, oneof_group: typing.Literal["_set_altitude", b"_set_altitude"]) -> typing.Literal["set_altitude"] | None: ... - @typing.overload - def WhichOneof(self, oneof_group: typing.Literal["_set_asc", b"_set_asc"]) -> typing.Literal["set_asc"] | None: ... - @typing.overload - def WhichOneof(self, oneof_group: typing.Literal["_set_measurement_interval", b"_set_measurement_interval"]) -> typing.Literal["set_measurement_interval"] | None: ... - @typing.overload - def WhichOneof(self, oneof_group: typing.Literal["_set_target_co2_conc", b"_set_target_co2_conc"]) -> typing.Literal["set_target_co2_conc"] | None: ... - @typing.overload - def WhichOneof(self, oneof_group: typing.Literal["_set_temperature", b"_set_temperature"]) -> typing.Literal["set_temperature"] | None: ... - @typing.overload - def WhichOneof(self, oneof_group: typing.Literal["_soft_reset", b"_soft_reset"]) -> typing.Literal["soft_reset"] | None: ... - -global___SCD30_config = SCD30_config diff --git a/meshtastic/protobuf/telemetry_pb2.py b/meshtastic/protobuf/telemetry_pb2.py index ea22e18eb..583215e88 100644 --- a/meshtastic/protobuf/telemetry_pb2.py +++ b/meshtastic/protobuf/telemetry_pb2.py @@ -13,7 +13,7 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n#meshtastic/protobuf/telemetry.proto\x12\x13meshtastic.protobuf\"\xf3\x01\n\rDeviceMetrics\x12\x1a\n\rbattery_level\x18\x01 \x01(\rH\x00\x88\x01\x01\x12\x14\n\x07voltage\x18\x02 \x01(\x02H\x01\x88\x01\x01\x12 \n\x13\x63hannel_utilization\x18\x03 \x01(\x02H\x02\x88\x01\x01\x12\x18\n\x0b\x61ir_util_tx\x18\x04 \x01(\x02H\x03\x88\x01\x01\x12\x1b\n\x0euptime_seconds\x18\x05 \x01(\rH\x04\x88\x01\x01\x42\x10\n\x0e_battery_levelB\n\n\x08_voltageB\x16\n\x14_channel_utilizationB\x0e\n\x0c_air_util_txB\x11\n\x0f_uptime_seconds\"\x82\x07\n\x12\x45nvironmentMetrics\x12\x18\n\x0btemperature\x18\x01 \x01(\x02H\x00\x88\x01\x01\x12\x1e\n\x11relative_humidity\x18\x02 \x01(\x02H\x01\x88\x01\x01\x12 \n\x13\x62\x61rometric_pressure\x18\x03 \x01(\x02H\x02\x88\x01\x01\x12\x1b\n\x0egas_resistance\x18\x04 \x01(\x02H\x03\x88\x01\x01\x12\x14\n\x07voltage\x18\x05 \x01(\x02H\x04\x88\x01\x01\x12\x14\n\x07\x63urrent\x18\x06 \x01(\x02H\x05\x88\x01\x01\x12\x10\n\x03iaq\x18\x07 \x01(\rH\x06\x88\x01\x01\x12\x15\n\x08\x64istance\x18\x08 \x01(\x02H\x07\x88\x01\x01\x12\x10\n\x03lux\x18\t \x01(\x02H\x08\x88\x01\x01\x12\x16\n\twhite_lux\x18\n \x01(\x02H\t\x88\x01\x01\x12\x13\n\x06ir_lux\x18\x0b \x01(\x02H\n\x88\x01\x01\x12\x13\n\x06uv_lux\x18\x0c \x01(\x02H\x0b\x88\x01\x01\x12\x1b\n\x0ewind_direction\x18\r \x01(\rH\x0c\x88\x01\x01\x12\x17\n\nwind_speed\x18\x0e \x01(\x02H\r\x88\x01\x01\x12\x13\n\x06weight\x18\x0f \x01(\x02H\x0e\x88\x01\x01\x12\x16\n\twind_gust\x18\x10 \x01(\x02H\x0f\x88\x01\x01\x12\x16\n\twind_lull\x18\x11 \x01(\x02H\x10\x88\x01\x01\x12\x16\n\tradiation\x18\x12 \x01(\x02H\x11\x88\x01\x01\x12\x18\n\x0brainfall_1h\x18\x13 \x01(\x02H\x12\x88\x01\x01\x12\x19\n\x0crainfall_24h\x18\x14 \x01(\x02H\x13\x88\x01\x01\x12\x1a\n\rsoil_moisture\x18\x15 \x01(\rH\x14\x88\x01\x01\x12\x1d\n\x10soil_temperature\x18\x16 \x01(\x02H\x15\x88\x01\x01\x42\x0e\n\x0c_temperatureB\x14\n\x12_relative_humidityB\x16\n\x14_barometric_pressureB\x11\n\x0f_gas_resistanceB\n\n\x08_voltageB\n\n\x08_currentB\x06\n\x04_iaqB\x0b\n\t_distanceB\x06\n\x04_luxB\x0c\n\n_white_luxB\t\n\x07_ir_luxB\t\n\x07_uv_luxB\x11\n\x0f_wind_directionB\r\n\x0b_wind_speedB\t\n\x07_weightB\x0c\n\n_wind_gustB\x0c\n\n_wind_lullB\x0c\n\n_radiationB\x0e\n\x0c_rainfall_1hB\x0f\n\r_rainfall_24hB\x10\n\x0e_soil_moistureB\x13\n\x11_soil_temperature\"\xae\x05\n\x0cPowerMetrics\x12\x18\n\x0b\x63h1_voltage\x18\x01 \x01(\x02H\x00\x88\x01\x01\x12\x18\n\x0b\x63h1_current\x18\x02 \x01(\x02H\x01\x88\x01\x01\x12\x18\n\x0b\x63h2_voltage\x18\x03 \x01(\x02H\x02\x88\x01\x01\x12\x18\n\x0b\x63h2_current\x18\x04 \x01(\x02H\x03\x88\x01\x01\x12\x18\n\x0b\x63h3_voltage\x18\x05 \x01(\x02H\x04\x88\x01\x01\x12\x18\n\x0b\x63h3_current\x18\x06 \x01(\x02H\x05\x88\x01\x01\x12\x18\n\x0b\x63h4_voltage\x18\x07 \x01(\x02H\x06\x88\x01\x01\x12\x18\n\x0b\x63h4_current\x18\x08 \x01(\x02H\x07\x88\x01\x01\x12\x18\n\x0b\x63h5_voltage\x18\t \x01(\x02H\x08\x88\x01\x01\x12\x18\n\x0b\x63h5_current\x18\n \x01(\x02H\t\x88\x01\x01\x12\x18\n\x0b\x63h6_voltage\x18\x0b \x01(\x02H\n\x88\x01\x01\x12\x18\n\x0b\x63h6_current\x18\x0c \x01(\x02H\x0b\x88\x01\x01\x12\x18\n\x0b\x63h7_voltage\x18\r \x01(\x02H\x0c\x88\x01\x01\x12\x18\n\x0b\x63h7_current\x18\x0e \x01(\x02H\r\x88\x01\x01\x12\x18\n\x0b\x63h8_voltage\x18\x0f \x01(\x02H\x0e\x88\x01\x01\x12\x18\n\x0b\x63h8_current\x18\x10 \x01(\x02H\x0f\x88\x01\x01\x42\x0e\n\x0c_ch1_voltageB\x0e\n\x0c_ch1_currentB\x0e\n\x0c_ch2_voltageB\x0e\n\x0c_ch2_currentB\x0e\n\x0c_ch3_voltageB\x0e\n\x0c_ch3_currentB\x0e\n\x0c_ch4_voltageB\x0e\n\x0c_ch4_currentB\x0e\n\x0c_ch5_voltageB\x0e\n\x0c_ch5_currentB\x0e\n\x0c_ch6_voltageB\x0e\n\x0c_ch6_currentB\x0e\n\x0c_ch7_voltageB\x0e\n\x0c_ch7_currentB\x0e\n\x0c_ch8_voltageB\x0e\n\x0c_ch8_current\"\xb1\t\n\x11\x41irQualityMetrics\x12\x1a\n\rpm10_standard\x18\x01 \x01(\rH\x00\x88\x01\x01\x12\x1a\n\rpm25_standard\x18\x02 \x01(\rH\x01\x88\x01\x01\x12\x1b\n\x0epm100_standard\x18\x03 \x01(\rH\x02\x88\x01\x01\x12\x1f\n\x12pm10_environmental\x18\x04 \x01(\rH\x03\x88\x01\x01\x12\x1f\n\x12pm25_environmental\x18\x05 \x01(\rH\x04\x88\x01\x01\x12 \n\x13pm100_environmental\x18\x06 \x01(\rH\x05\x88\x01\x01\x12\x1b\n\x0eparticles_03um\x18\x07 \x01(\rH\x06\x88\x01\x01\x12\x1b\n\x0eparticles_05um\x18\x08 \x01(\rH\x07\x88\x01\x01\x12\x1b\n\x0eparticles_10um\x18\t \x01(\rH\x08\x88\x01\x01\x12\x1b\n\x0eparticles_25um\x18\n \x01(\rH\t\x88\x01\x01\x12\x1b\n\x0eparticles_50um\x18\x0b \x01(\rH\n\x88\x01\x01\x12\x1c\n\x0fparticles_100um\x18\x0c \x01(\rH\x0b\x88\x01\x01\x12\x10\n\x03\x63o2\x18\r \x01(\rH\x0c\x88\x01\x01\x12\x1c\n\x0f\x63o2_temperature\x18\x0e \x01(\x02H\r\x88\x01\x01\x12\x19\n\x0c\x63o2_humidity\x18\x0f \x01(\x02H\x0e\x88\x01\x01\x12\x1e\n\x11\x66orm_formaldehyde\x18\x10 \x01(\x02H\x0f\x88\x01\x01\x12\x1a\n\rform_humidity\x18\x11 \x01(\x02H\x10\x88\x01\x01\x12\x1d\n\x10\x66orm_temperature\x18\x12 \x01(\x02H\x11\x88\x01\x01\x12\x1a\n\rpm40_standard\x18\x13 \x01(\rH\x12\x88\x01\x01\x12\x1b\n\x0eparticles_40um\x18\x14 \x01(\rH\x13\x88\x01\x01\x12\x1b\n\x0epm_temperature\x18\x15 \x01(\x02H\x14\x88\x01\x01\x12\x18\n\x0bpm_humidity\x18\x16 \x01(\x02H\x15\x88\x01\x01\x12\x17\n\npm_voc_idx\x18\x17 \x01(\x02H\x16\x88\x01\x01\x12\x17\n\npm_nox_idx\x18\x18 \x01(\x02H\x17\x88\x01\x01\x12\x1a\n\rparticles_tps\x18\x19 \x01(\x02H\x18\x88\x01\x01\x42\x10\n\x0e_pm10_standardB\x10\n\x0e_pm25_standardB\x11\n\x0f_pm100_standardB\x15\n\x13_pm10_environmentalB\x15\n\x13_pm25_environmentalB\x16\n\x14_pm100_environmentalB\x11\n\x0f_particles_03umB\x11\n\x0f_particles_05umB\x11\n\x0f_particles_10umB\x11\n\x0f_particles_25umB\x11\n\x0f_particles_50umB\x12\n\x10_particles_100umB\x06\n\x04_co2B\x12\n\x10_co2_temperatureB\x0f\n\r_co2_humidityB\x14\n\x12_form_formaldehydeB\x10\n\x0e_form_humidityB\x13\n\x11_form_temperatureB\x10\n\x0e_pm40_standardB\x11\n\x0f_particles_40umB\x11\n\x0f_pm_temperatureB\x0e\n\x0c_pm_humidityB\r\n\x0b_pm_voc_idxB\r\n\x0b_pm_nox_idxB\x10\n\x0e_particles_tps\"\xff\x02\n\nLocalStats\x12\x16\n\x0euptime_seconds\x18\x01 \x01(\r\x12\x1b\n\x13\x63hannel_utilization\x18\x02 \x01(\x02\x12\x13\n\x0b\x61ir_util_tx\x18\x03 \x01(\x02\x12\x16\n\x0enum_packets_tx\x18\x04 \x01(\r\x12\x16\n\x0enum_packets_rx\x18\x05 \x01(\r\x12\x1a\n\x12num_packets_rx_bad\x18\x06 \x01(\r\x12\x18\n\x10num_online_nodes\x18\x07 \x01(\r\x12\x17\n\x0fnum_total_nodes\x18\x08 \x01(\r\x12\x13\n\x0bnum_rx_dupe\x18\t \x01(\r\x12\x14\n\x0cnum_tx_relay\x18\n \x01(\r\x12\x1d\n\x15num_tx_relay_canceled\x18\x0b \x01(\r\x12\x18\n\x10heap_total_bytes\x18\x0c \x01(\r\x12\x17\n\x0fheap_free_bytes\x18\r \x01(\r\x12\x16\n\x0enum_tx_dropped\x18\x0e \x01(\r\x12\x13\n\x0bnoise_floor\x18\x0f \x01(\x05\"\xe4\x01\n\x16TrafficManagementStats\x12\x19\n\x11packets_inspected\x18\x01 \x01(\r\x12\x1c\n\x14position_dedup_drops\x18\x02 \x01(\r\x12\x1b\n\x13nodeinfo_cache_hits\x18\x03 \x01(\r\x12\x18\n\x10rate_limit_drops\x18\x04 \x01(\r\x12\x1c\n\x14unknown_packet_drops\x18\x05 \x01(\r\x12\x1d\n\x15hop_exhausted_packets\x18\x06 \x01(\r\x12\x1d\n\x15router_hops_preserved\x18\x07 \x01(\r\"{\n\rHealthMetrics\x12\x16\n\theart_bpm\x18\x01 \x01(\rH\x00\x88\x01\x01\x12\x11\n\x04spO2\x18\x02 \x01(\rH\x01\x88\x01\x01\x12\x18\n\x0btemperature\x18\x03 \x01(\x02H\x02\x88\x01\x01\x42\x0c\n\n_heart_bpmB\x07\n\x05_spO2B\x0e\n\x0c_temperature\"\x91\x02\n\x0bHostMetrics\x12\x16\n\x0euptime_seconds\x18\x01 \x01(\r\x12\x15\n\rfreemem_bytes\x18\x02 \x01(\x04\x12\x17\n\x0f\x64iskfree1_bytes\x18\x03 \x01(\x04\x12\x1c\n\x0f\x64iskfree2_bytes\x18\x04 \x01(\x04H\x00\x88\x01\x01\x12\x1c\n\x0f\x64iskfree3_bytes\x18\x05 \x01(\x04H\x01\x88\x01\x01\x12\r\n\x05load1\x18\x06 \x01(\r\x12\r\n\x05load5\x18\x07 \x01(\r\x12\x0e\n\x06load15\x18\x08 \x01(\r\x12\x18\n\x0buser_string\x18\t \x01(\tH\x02\x88\x01\x01\x42\x12\n\x10_diskfree2_bytesB\x12\n\x10_diskfree3_bytesB\x0e\n\x0c_user_string\"\xae\x04\n\tTelemetry\x12\x0c\n\x04time\x18\x01 \x01(\x07\x12<\n\x0e\x64\x65vice_metrics\x18\x02 \x01(\x0b\x32\".meshtastic.protobuf.DeviceMetricsH\x00\x12\x46\n\x13\x65nvironment_metrics\x18\x03 \x01(\x0b\x32\'.meshtastic.protobuf.EnvironmentMetricsH\x00\x12\x45\n\x13\x61ir_quality_metrics\x18\x04 \x01(\x0b\x32&.meshtastic.protobuf.AirQualityMetricsH\x00\x12:\n\rpower_metrics\x18\x05 \x01(\x0b\x32!.meshtastic.protobuf.PowerMetricsH\x00\x12\x36\n\x0blocal_stats\x18\x06 \x01(\x0b\x32\x1f.meshtastic.protobuf.LocalStatsH\x00\x12<\n\x0ehealth_metrics\x18\x07 \x01(\x0b\x32\".meshtastic.protobuf.HealthMetricsH\x00\x12\x38\n\x0chost_metrics\x18\x08 \x01(\x0b\x32 .meshtastic.protobuf.HostMetricsH\x00\x12O\n\x18traffic_management_stats\x18\t \x01(\x0b\x32+.meshtastic.protobuf.TrafficManagementStatsH\x00\x42\t\n\x07variant\">\n\rNau7802Config\x12\x12\n\nzeroOffset\x18\x01 \x01(\x05\x12\x19\n\x11\x63\x61librationFactor\x18\x02 \x01(\x02\"\xf0\x01\n\nSEN5XState\x12\x1a\n\x12last_cleaning_time\x18\x01 \x01(\r\x12\x1b\n\x13last_cleaning_valid\x18\x02 \x01(\x08\x12\x15\n\rone_shot_mode\x18\x03 \x01(\x08\x12\x1b\n\x0evoc_state_time\x18\x04 \x01(\rH\x00\x88\x01\x01\x12\x1c\n\x0fvoc_state_valid\x18\x05 \x01(\x08H\x01\x88\x01\x01\x12\x1c\n\x0fvoc_state_array\x18\x06 \x01(\x06H\x02\x88\x01\x01\x42\x11\n\x0f_voc_state_timeB\x12\n\x10_voc_state_validB\x12\n\x10_voc_state_array*\xa7\x05\n\x13TelemetrySensorType\x12\x10\n\x0cSENSOR_UNSET\x10\x00\x12\n\n\x06\x42ME280\x10\x01\x12\n\n\x06\x42ME680\x10\x02\x12\x0b\n\x07MCP9808\x10\x03\x12\n\n\x06INA260\x10\x04\x12\n\n\x06INA219\x10\x05\x12\n\n\x06\x42MP280\x10\x06\x12\t\n\x05SHTC3\x10\x07\x12\t\n\x05LPS22\x10\x08\x12\x0b\n\x07QMC6310\x10\t\x12\x0b\n\x07QMI8658\x10\n\x12\x0c\n\x08QMC5883L\x10\x0b\x12\t\n\x05SHT31\x10\x0c\x12\x0c\n\x08PMSA003I\x10\r\x12\x0b\n\x07INA3221\x10\x0e\x12\n\n\x06\x42MP085\x10\x0f\x12\x0c\n\x08RCWL9620\x10\x10\x12\t\n\x05SHT4X\x10\x11\x12\x0c\n\x08VEML7700\x10\x12\x12\x0c\n\x08MLX90632\x10\x13\x12\x0b\n\x07OPT3001\x10\x14\x12\x0c\n\x08LTR390UV\x10\x15\x12\x0e\n\nTSL25911FN\x10\x16\x12\t\n\x05\x41HT10\x10\x17\x12\x10\n\x0c\x44\x46ROBOT_LARK\x10\x18\x12\x0b\n\x07NAU7802\x10\x19\x12\n\n\x06\x42MP3XX\x10\x1a\x12\x0c\n\x08ICM20948\x10\x1b\x12\x0c\n\x08MAX17048\x10\x1c\x12\x11\n\rCUSTOM_SENSOR\x10\x1d\x12\x0c\n\x08MAX30102\x10\x1e\x12\x0c\n\x08MLX90614\x10\x1f\x12\t\n\x05SCD4X\x10 \x12\x0b\n\x07RADSENS\x10!\x12\n\n\x06INA226\x10\"\x12\x10\n\x0c\x44\x46ROBOT_RAIN\x10#\x12\n\n\x06\x44PS310\x10$\x12\x0c\n\x08RAK12035\x10%\x12\x0c\n\x08MAX17261\x10&\x12\x0b\n\x07PCT2075\x10\'\x12\x0b\n\x07\x41\x44S1X15\x10(\x12\x0f\n\x0b\x41\x44S1X15_ALT\x10)\x12\t\n\x05SFA30\x10*\x12\t\n\x05SEN5X\x10+\x12\x0b\n\x07TSL2561\x10,\x12\n\n\x06\x42H1750\x10-\x12\x0b\n\x07HDC1080\x10.\x12\t\n\x05SHT21\x10/\x12\t\n\x05STC31\x10\x30\x12\t\n\x05SCD30\x10\x31\x42\x65\n\x14org.meshtastic.protoB\x0fTelemetryProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n#meshtastic/protobuf/telemetry.proto\x12\x13meshtastic.protobuf\"\xf3\x01\n\rDeviceMetrics\x12\x1a\n\rbattery_level\x18\x01 \x01(\rH\x00\x88\x01\x01\x12\x14\n\x07voltage\x18\x02 \x01(\x02H\x01\x88\x01\x01\x12 \n\x13\x63hannel_utilization\x18\x03 \x01(\x02H\x02\x88\x01\x01\x12\x18\n\x0b\x61ir_util_tx\x18\x04 \x01(\x02H\x03\x88\x01\x01\x12\x1b\n\x0euptime_seconds\x18\x05 \x01(\rH\x04\x88\x01\x01\x42\x10\n\x0e_battery_levelB\n\n\x08_voltageB\x16\n\x14_channel_utilizationB\x0e\n\x0c_air_util_txB\x11\n\x0f_uptime_seconds\"\x82\x07\n\x12\x45nvironmentMetrics\x12\x18\n\x0btemperature\x18\x01 \x01(\x02H\x00\x88\x01\x01\x12\x1e\n\x11relative_humidity\x18\x02 \x01(\x02H\x01\x88\x01\x01\x12 \n\x13\x62\x61rometric_pressure\x18\x03 \x01(\x02H\x02\x88\x01\x01\x12\x1b\n\x0egas_resistance\x18\x04 \x01(\x02H\x03\x88\x01\x01\x12\x14\n\x07voltage\x18\x05 \x01(\x02H\x04\x88\x01\x01\x12\x14\n\x07\x63urrent\x18\x06 \x01(\x02H\x05\x88\x01\x01\x12\x10\n\x03iaq\x18\x07 \x01(\rH\x06\x88\x01\x01\x12\x15\n\x08\x64istance\x18\x08 \x01(\x02H\x07\x88\x01\x01\x12\x10\n\x03lux\x18\t \x01(\x02H\x08\x88\x01\x01\x12\x16\n\twhite_lux\x18\n \x01(\x02H\t\x88\x01\x01\x12\x13\n\x06ir_lux\x18\x0b \x01(\x02H\n\x88\x01\x01\x12\x13\n\x06uv_lux\x18\x0c \x01(\x02H\x0b\x88\x01\x01\x12\x1b\n\x0ewind_direction\x18\r \x01(\rH\x0c\x88\x01\x01\x12\x17\n\nwind_speed\x18\x0e \x01(\x02H\r\x88\x01\x01\x12\x13\n\x06weight\x18\x0f \x01(\x02H\x0e\x88\x01\x01\x12\x16\n\twind_gust\x18\x10 \x01(\x02H\x0f\x88\x01\x01\x12\x16\n\twind_lull\x18\x11 \x01(\x02H\x10\x88\x01\x01\x12\x16\n\tradiation\x18\x12 \x01(\x02H\x11\x88\x01\x01\x12\x18\n\x0brainfall_1h\x18\x13 \x01(\x02H\x12\x88\x01\x01\x12\x19\n\x0crainfall_24h\x18\x14 \x01(\x02H\x13\x88\x01\x01\x12\x1a\n\rsoil_moisture\x18\x15 \x01(\rH\x14\x88\x01\x01\x12\x1d\n\x10soil_temperature\x18\x16 \x01(\x02H\x15\x88\x01\x01\x42\x0e\n\x0c_temperatureB\x14\n\x12_relative_humidityB\x16\n\x14_barometric_pressureB\x11\n\x0f_gas_resistanceB\n\n\x08_voltageB\n\n\x08_currentB\x06\n\x04_iaqB\x0b\n\t_distanceB\x06\n\x04_luxB\x0c\n\n_white_luxB\t\n\x07_ir_luxB\t\n\x07_uv_luxB\x11\n\x0f_wind_directionB\r\n\x0b_wind_speedB\t\n\x07_weightB\x0c\n\n_wind_gustB\x0c\n\n_wind_lullB\x0c\n\n_radiationB\x0e\n\x0c_rainfall_1hB\x0f\n\r_rainfall_24hB\x10\n\x0e_soil_moistureB\x13\n\x11_soil_temperature\"\xae\x05\n\x0cPowerMetrics\x12\x18\n\x0b\x63h1_voltage\x18\x01 \x01(\x02H\x00\x88\x01\x01\x12\x18\n\x0b\x63h1_current\x18\x02 \x01(\x02H\x01\x88\x01\x01\x12\x18\n\x0b\x63h2_voltage\x18\x03 \x01(\x02H\x02\x88\x01\x01\x12\x18\n\x0b\x63h2_current\x18\x04 \x01(\x02H\x03\x88\x01\x01\x12\x18\n\x0b\x63h3_voltage\x18\x05 \x01(\x02H\x04\x88\x01\x01\x12\x18\n\x0b\x63h3_current\x18\x06 \x01(\x02H\x05\x88\x01\x01\x12\x18\n\x0b\x63h4_voltage\x18\x07 \x01(\x02H\x06\x88\x01\x01\x12\x18\n\x0b\x63h4_current\x18\x08 \x01(\x02H\x07\x88\x01\x01\x12\x18\n\x0b\x63h5_voltage\x18\t \x01(\x02H\x08\x88\x01\x01\x12\x18\n\x0b\x63h5_current\x18\n \x01(\x02H\t\x88\x01\x01\x12\x18\n\x0b\x63h6_voltage\x18\x0b \x01(\x02H\n\x88\x01\x01\x12\x18\n\x0b\x63h6_current\x18\x0c \x01(\x02H\x0b\x88\x01\x01\x12\x18\n\x0b\x63h7_voltage\x18\r \x01(\x02H\x0c\x88\x01\x01\x12\x18\n\x0b\x63h7_current\x18\x0e \x01(\x02H\r\x88\x01\x01\x12\x18\n\x0b\x63h8_voltage\x18\x0f \x01(\x02H\x0e\x88\x01\x01\x12\x18\n\x0b\x63h8_current\x18\x10 \x01(\x02H\x0f\x88\x01\x01\x42\x0e\n\x0c_ch1_voltageB\x0e\n\x0c_ch1_currentB\x0e\n\x0c_ch2_voltageB\x0e\n\x0c_ch2_currentB\x0e\n\x0c_ch3_voltageB\x0e\n\x0c_ch3_currentB\x0e\n\x0c_ch4_voltageB\x0e\n\x0c_ch4_currentB\x0e\n\x0c_ch5_voltageB\x0e\n\x0c_ch5_currentB\x0e\n\x0c_ch6_voltageB\x0e\n\x0c_ch6_currentB\x0e\n\x0c_ch7_voltageB\x0e\n\x0c_ch7_currentB\x0e\n\x0c_ch8_voltageB\x0e\n\x0c_ch8_current\"\xb1\t\n\x11\x41irQualityMetrics\x12\x1a\n\rpm10_standard\x18\x01 \x01(\rH\x00\x88\x01\x01\x12\x1a\n\rpm25_standard\x18\x02 \x01(\rH\x01\x88\x01\x01\x12\x1b\n\x0epm100_standard\x18\x03 \x01(\rH\x02\x88\x01\x01\x12\x1f\n\x12pm10_environmental\x18\x04 \x01(\rH\x03\x88\x01\x01\x12\x1f\n\x12pm25_environmental\x18\x05 \x01(\rH\x04\x88\x01\x01\x12 \n\x13pm100_environmental\x18\x06 \x01(\rH\x05\x88\x01\x01\x12\x1b\n\x0eparticles_03um\x18\x07 \x01(\rH\x06\x88\x01\x01\x12\x1b\n\x0eparticles_05um\x18\x08 \x01(\rH\x07\x88\x01\x01\x12\x1b\n\x0eparticles_10um\x18\t \x01(\rH\x08\x88\x01\x01\x12\x1b\n\x0eparticles_25um\x18\n \x01(\rH\t\x88\x01\x01\x12\x1b\n\x0eparticles_50um\x18\x0b \x01(\rH\n\x88\x01\x01\x12\x1c\n\x0fparticles_100um\x18\x0c \x01(\rH\x0b\x88\x01\x01\x12\x10\n\x03\x63o2\x18\r \x01(\rH\x0c\x88\x01\x01\x12\x1c\n\x0f\x63o2_temperature\x18\x0e \x01(\x02H\r\x88\x01\x01\x12\x19\n\x0c\x63o2_humidity\x18\x0f \x01(\x02H\x0e\x88\x01\x01\x12\x1e\n\x11\x66orm_formaldehyde\x18\x10 \x01(\x02H\x0f\x88\x01\x01\x12\x1a\n\rform_humidity\x18\x11 \x01(\x02H\x10\x88\x01\x01\x12\x1d\n\x10\x66orm_temperature\x18\x12 \x01(\x02H\x11\x88\x01\x01\x12\x1a\n\rpm40_standard\x18\x13 \x01(\rH\x12\x88\x01\x01\x12\x1b\n\x0eparticles_40um\x18\x14 \x01(\rH\x13\x88\x01\x01\x12\x1b\n\x0epm_temperature\x18\x15 \x01(\x02H\x14\x88\x01\x01\x12\x18\n\x0bpm_humidity\x18\x16 \x01(\x02H\x15\x88\x01\x01\x12\x17\n\npm_voc_idx\x18\x17 \x01(\x02H\x16\x88\x01\x01\x12\x17\n\npm_nox_idx\x18\x18 \x01(\x02H\x17\x88\x01\x01\x12\x1a\n\rparticles_tps\x18\x19 \x01(\x02H\x18\x88\x01\x01\x42\x10\n\x0e_pm10_standardB\x10\n\x0e_pm25_standardB\x11\n\x0f_pm100_standardB\x15\n\x13_pm10_environmentalB\x15\n\x13_pm25_environmentalB\x16\n\x14_pm100_environmentalB\x11\n\x0f_particles_03umB\x11\n\x0f_particles_05umB\x11\n\x0f_particles_10umB\x11\n\x0f_particles_25umB\x11\n\x0f_particles_50umB\x12\n\x10_particles_100umB\x06\n\x04_co2B\x12\n\x10_co2_temperatureB\x0f\n\r_co2_humidityB\x14\n\x12_form_formaldehydeB\x10\n\x0e_form_humidityB\x13\n\x11_form_temperatureB\x10\n\x0e_pm40_standardB\x11\n\x0f_particles_40umB\x11\n\x0f_pm_temperatureB\x0e\n\x0c_pm_humidityB\r\n\x0b_pm_voc_idxB\r\n\x0b_pm_nox_idxB\x10\n\x0e_particles_tps\"\xff\x02\n\nLocalStats\x12\x16\n\x0euptime_seconds\x18\x01 \x01(\r\x12\x1b\n\x13\x63hannel_utilization\x18\x02 \x01(\x02\x12\x13\n\x0b\x61ir_util_tx\x18\x03 \x01(\x02\x12\x16\n\x0enum_packets_tx\x18\x04 \x01(\r\x12\x16\n\x0enum_packets_rx\x18\x05 \x01(\r\x12\x1a\n\x12num_packets_rx_bad\x18\x06 \x01(\r\x12\x18\n\x10num_online_nodes\x18\x07 \x01(\r\x12\x17\n\x0fnum_total_nodes\x18\x08 \x01(\r\x12\x13\n\x0bnum_rx_dupe\x18\t \x01(\r\x12\x14\n\x0cnum_tx_relay\x18\n \x01(\r\x12\x1d\n\x15num_tx_relay_canceled\x18\x0b \x01(\r\x12\x18\n\x10heap_total_bytes\x18\x0c \x01(\r\x12\x17\n\x0fheap_free_bytes\x18\r \x01(\r\x12\x16\n\x0enum_tx_dropped\x18\x0e \x01(\r\x12\x13\n\x0bnoise_floor\x18\x0f \x01(\x05\"\xe4\x01\n\x16TrafficManagementStats\x12\x19\n\x11packets_inspected\x18\x01 \x01(\r\x12\x1c\n\x14position_dedup_drops\x18\x02 \x01(\r\x12\x1b\n\x13nodeinfo_cache_hits\x18\x03 \x01(\r\x12\x18\n\x10rate_limit_drops\x18\x04 \x01(\r\x12\x1c\n\x14unknown_packet_drops\x18\x05 \x01(\r\x12\x1d\n\x15hop_exhausted_packets\x18\x06 \x01(\r\x12\x1d\n\x15router_hops_preserved\x18\x07 \x01(\r\"{\n\rHealthMetrics\x12\x16\n\theart_bpm\x18\x01 \x01(\rH\x00\x88\x01\x01\x12\x11\n\x04spO2\x18\x02 \x01(\rH\x01\x88\x01\x01\x12\x18\n\x0btemperature\x18\x03 \x01(\x02H\x02\x88\x01\x01\x42\x0c\n\n_heart_bpmB\x07\n\x05_spO2B\x0e\n\x0c_temperature\"\x91\x02\n\x0bHostMetrics\x12\x16\n\x0euptime_seconds\x18\x01 \x01(\r\x12\x15\n\rfreemem_bytes\x18\x02 \x01(\x04\x12\x17\n\x0f\x64iskfree1_bytes\x18\x03 \x01(\x04\x12\x1c\n\x0f\x64iskfree2_bytes\x18\x04 \x01(\x04H\x00\x88\x01\x01\x12\x1c\n\x0f\x64iskfree3_bytes\x18\x05 \x01(\x04H\x01\x88\x01\x01\x12\r\n\x05load1\x18\x06 \x01(\r\x12\r\n\x05load5\x18\x07 \x01(\r\x12\x0e\n\x06load15\x18\x08 \x01(\r\x12\x18\n\x0buser_string\x18\t \x01(\tH\x02\x88\x01\x01\x42\x12\n\x10_diskfree2_bytesB\x12\n\x10_diskfree3_bytesB\x0e\n\x0c_user_string\"\xae\x04\n\tTelemetry\x12\x0c\n\x04time\x18\x01 \x01(\x07\x12<\n\x0e\x64\x65vice_metrics\x18\x02 \x01(\x0b\x32\".meshtastic.protobuf.DeviceMetricsH\x00\x12\x46\n\x13\x65nvironment_metrics\x18\x03 \x01(\x0b\x32\'.meshtastic.protobuf.EnvironmentMetricsH\x00\x12\x45\n\x13\x61ir_quality_metrics\x18\x04 \x01(\x0b\x32&.meshtastic.protobuf.AirQualityMetricsH\x00\x12:\n\rpower_metrics\x18\x05 \x01(\x0b\x32!.meshtastic.protobuf.PowerMetricsH\x00\x12\x36\n\x0blocal_stats\x18\x06 \x01(\x0b\x32\x1f.meshtastic.protobuf.LocalStatsH\x00\x12<\n\x0ehealth_metrics\x18\x07 \x01(\x0b\x32\".meshtastic.protobuf.HealthMetricsH\x00\x12\x38\n\x0chost_metrics\x18\x08 \x01(\x0b\x32 .meshtastic.protobuf.HostMetricsH\x00\x12O\n\x18traffic_management_stats\x18\t \x01(\x0b\x32+.meshtastic.protobuf.TrafficManagementStatsH\x00\x42\t\n\x07variant\">\n\rNau7802Config\x12\x12\n\nzeroOffset\x18\x01 \x01(\x05\x12\x19\n\x11\x63\x61librationFactor\x18\x02 \x01(\x02\"\xf0\x01\n\nSEN5XState\x12\x1a\n\x12last_cleaning_time\x18\x01 \x01(\r\x12\x1b\n\x13last_cleaning_valid\x18\x02 \x01(\x08\x12\x15\n\rone_shot_mode\x18\x03 \x01(\x08\x12\x1b\n\x0evoc_state_time\x18\x04 \x01(\rH\x00\x88\x01\x01\x12\x1c\n\x0fvoc_state_valid\x18\x05 \x01(\x08H\x01\x88\x01\x01\x12\x1c\n\x0fvoc_state_array\x18\x06 \x01(\x06H\x02\x88\x01\x01\x42\x11\n\x0f_voc_state_timeB\x12\n\x10_voc_state_validB\x12\n\x10_voc_state_array*\x9c\x05\n\x13TelemetrySensorType\x12\x10\n\x0cSENSOR_UNSET\x10\x00\x12\n\n\x06\x42ME280\x10\x01\x12\n\n\x06\x42ME680\x10\x02\x12\x0b\n\x07MCP9808\x10\x03\x12\n\n\x06INA260\x10\x04\x12\n\n\x06INA219\x10\x05\x12\n\n\x06\x42MP280\x10\x06\x12\t\n\x05SHTC3\x10\x07\x12\t\n\x05LPS22\x10\x08\x12\x0b\n\x07QMC6310\x10\t\x12\x0b\n\x07QMI8658\x10\n\x12\x0c\n\x08QMC5883L\x10\x0b\x12\t\n\x05SHT31\x10\x0c\x12\x0c\n\x08PMSA003I\x10\r\x12\x0b\n\x07INA3221\x10\x0e\x12\n\n\x06\x42MP085\x10\x0f\x12\x0c\n\x08RCWL9620\x10\x10\x12\t\n\x05SHT4X\x10\x11\x12\x0c\n\x08VEML7700\x10\x12\x12\x0c\n\x08MLX90632\x10\x13\x12\x0b\n\x07OPT3001\x10\x14\x12\x0c\n\x08LTR390UV\x10\x15\x12\x0e\n\nTSL25911FN\x10\x16\x12\t\n\x05\x41HT10\x10\x17\x12\x10\n\x0c\x44\x46ROBOT_LARK\x10\x18\x12\x0b\n\x07NAU7802\x10\x19\x12\n\n\x06\x42MP3XX\x10\x1a\x12\x0c\n\x08ICM20948\x10\x1b\x12\x0c\n\x08MAX17048\x10\x1c\x12\x11\n\rCUSTOM_SENSOR\x10\x1d\x12\x0c\n\x08MAX30102\x10\x1e\x12\x0c\n\x08MLX90614\x10\x1f\x12\t\n\x05SCD4X\x10 \x12\x0b\n\x07RADSENS\x10!\x12\n\n\x06INA226\x10\"\x12\x10\n\x0c\x44\x46ROBOT_RAIN\x10#\x12\n\n\x06\x44PS310\x10$\x12\x0c\n\x08RAK12035\x10%\x12\x0c\n\x08MAX17261\x10&\x12\x0b\n\x07PCT2075\x10\'\x12\x0b\n\x07\x41\x44S1X15\x10(\x12\x0f\n\x0b\x41\x44S1X15_ALT\x10)\x12\t\n\x05SFA30\x10*\x12\t\n\x05SEN5X\x10+\x12\x0b\n\x07TSL2561\x10,\x12\n\n\x06\x42H1750\x10-\x12\x0b\n\x07HDC1080\x10.\x12\t\n\x05SHT21\x10/\x12\t\n\x05STC31\x10\x30\x42\x65\n\x14org.meshtastic.protoB\x0fTelemetryProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -22,7 +22,7 @@ DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\017TelemetryProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000' _globals['_TELEMETRYSENSORTYPE']._serialized_start=4987 - _globals['_TELEMETRYSENSORTYPE']._serialized_end=5666 + _globals['_TELEMETRYSENSORTYPE']._serialized_end=5655 _globals['_DEVICEMETRICS']._serialized_start=61 _globals['_DEVICEMETRICS']._serialized_end=304 _globals['_ENVIRONMENTMETRICS']._serialized_start=307 diff --git a/meshtastic/protobuf/telemetry_pb2.pyi b/meshtastic/protobuf/telemetry_pb2.pyi index 333841084..322fdbfd6 100644 --- a/meshtastic/protobuf/telemetry_pb2.pyi +++ b/meshtastic/protobuf/telemetry_pb2.pyi @@ -219,10 +219,6 @@ class _TelemetrySensorTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wra """ Sensirion STC31 CO2 sensor """ - SCD30: _TelemetrySensorType.ValueType # 49 - """ - SCD30 CO2, humidity, temperature sensor - """ class TelemetrySensorType(_TelemetrySensorType, metaclass=_TelemetrySensorTypeEnumTypeWrapper): """ @@ -425,10 +421,6 @@ STC31: TelemetrySensorType.ValueType # 48 """ Sensirion STC31 CO2 sensor """ -SCD30: TelemetrySensorType.ValueType # 49 -""" -SCD30 CO2, humidity, temperature sensor -""" global___TelemetrySensorType = TelemetrySensorType @typing.final diff --git a/meshtastic/tests/test_main.py b/meshtastic/tests/test_main.py index 251de98fe..048614e5d 100644 --- a/meshtastic/tests/test_main.py +++ b/meshtastic/tests/test_main.py @@ -6,6 +6,7 @@ import platform import re import sys +import tempfile from unittest.mock import mock_open, MagicMock, patch import pytest @@ -2900,3 +2901,68 @@ def test_main_set_ham_empty_string(capsys): out, _ = capsys.readouterr() assert "ERROR: Ham radio callsign cannot be empty or contain only whitespace characters" in out assert excinfo.value.code == 1 + + +# OTA-related tests +@pytest.mark.unit +@pytest.mark.usefixtures("reset_mt_config") +def test_main_ota_update_file_not_found(capsys): + """Test --ota-update with non-existent file""" + sys.argv = [ + "", + "--ota-update", + "/nonexistent/firmware.bin", + "--host", + "192.168.1.100", + ] + mt_config.args = sys.argv + + with pytest.raises(SystemExit) as pytest_wrapped_e: + main() + + assert pytest_wrapped_e.type == SystemExit + assert pytest_wrapped_e.value.code == 1 + + +@pytest.mark.unit +@pytest.mark.usefixtures("reset_mt_config") +@patch("meshtastic.ota.ESP32WiFiOTA") +@patch("meshtastic.__main__.meshtastic.util.our_exit") +def test_main_ota_update_retries(mock_our_exit, mock_ota_class, capsys): + """Test --ota-update retries on failure""" + # Create a temporary firmware file + with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f: + f.write(b"fake firmware data") + firmware_file = f.name + + try: + sys.argv = ["", "--ota-update", firmware_file, "--host", "192.168.1.100"] + mt_config.args = sys.argv + + # Mock the OTA class to fail all 5 retries + mock_ota = MagicMock() + mock_ota_class.return_value = mock_ota + mock_ota.hash_bytes.return_value = b"\x00" * 32 + mock_ota.hash_hex.return_value = "a" * 64 + mock_ota.update.side_effect = Exception("Connection failed") + + # Mock isinstance to return True + with patch("meshtastic.__main__.isinstance", return_value=True): + with patch("meshtastic.tcp_interface.TCPInterface") as mock_tcp: + mock_iface = MagicMock() + mock_iface.hostname = "192.168.1.100" + mock_iface.localNode = MagicMock(autospec=Node) + mock_tcp.return_value = mock_iface + + with patch("time.sleep"): + main() + + # Should have exhausted all retries and called our_exit + # Note: our_exit might be called twice - once for TCP check, once for failure + assert mock_our_exit.call_count >= 1 + # Check the last call was for OTA failure + last_call_args = mock_our_exit.call_args[0][0] + assert "OTA update failed" in last_call_args + + finally: + os.unlink(firmware_file) diff --git a/meshtastic/tests/test_mesh_interface_traffic_management.py b/meshtastic/tests/test_mesh_interface_traffic_management.py new file mode 100644 index 000000000..f1aa4f9bb --- /dev/null +++ b/meshtastic/tests/test_mesh_interface_traffic_management.py @@ -0,0 +1,22 @@ +"""Meshtastic unit tests for traffic management handling in mesh_interface.py.""" + +import pytest + +from ..mesh_interface import MeshInterface +from ..protobuf import mesh_pb2 + + +@pytest.mark.unit +@pytest.mark.usefixtures("reset_mt_config") +def test_handleFromRadio_with_traffic_management_module_config(): + """Test _handleFromRadio with moduleConfig.traffic_management.""" + iface = MeshInterface(noProto=True) + from_radio = mesh_pb2.FromRadio() + from_radio.moduleConfig.traffic_management.enabled = True + from_radio.moduleConfig.traffic_management.rate_limit_enabled = True + + iface._handleFromRadio(from_radio.SerializeToString()) + + assert iface.localNode.moduleConfig.traffic_management.enabled is True + assert iface.localNode.moduleConfig.traffic_management.rate_limit_enabled is True + iface.close() diff --git a/meshtastic/tests/test_node.py b/meshtastic/tests/test_node.py index c5cb6b3fa..986c1783c 100644 --- a/meshtastic/tests/test_node.py +++ b/meshtastic/tests/test_node.py @@ -1,4 +1,5 @@ """Meshtastic unit tests for node.py""" +# pylint: disable=C0302 import logging import re @@ -794,6 +795,30 @@ def test_writeConfig_with_no_radioConfig(capsys): assert err == "" +@pytest.mark.unit +@pytest.mark.usefixtures("reset_mt_config") +def test_writeConfig_traffic_management(): + """Test writeConfig with traffic_management module config.""" + iface = MagicMock(autospec=SerialInterface) + anode = Node(iface, 123, noProto=True) + anode.moduleConfig.traffic_management.enabled = True + anode.moduleConfig.traffic_management.rate_limit_enabled = True + + sent_admin = [] + + def capture_send(p, *args, **kwargs): # pylint: disable=W0613 + sent_admin.append(p) + + with patch.object(anode, "_sendAdmin", side_effect=capture_send): + anode.writeConfig("traffic_management") + + assert len(sent_admin) == 1 + assert sent_admin[0].HasField("set_module_config") + assert sent_admin[0].set_module_config.HasField("traffic_management") + assert sent_admin[0].set_module_config.traffic_management.enabled is True + assert sent_admin[0].set_module_config.traffic_management.rate_limit_enabled is True + + # TODO # @pytest.mark.unit # def test_writeConfig(caplog): @@ -1550,6 +1575,41 @@ def test_setOwner_valid_names(caplog): assert re.search(r'p.set_owner.short_name:VN:', caplog.text, re.MULTILINE) +@pytest.mark.unit +def test_start_ota_local_node(): + """Test startOTA on local node""" + iface = MagicMock(autospec=MeshInterface) + anode = Node(iface, 1234567890, noProto=True) + # Set up as local node + iface.localNode = anode + + amesg = admin_pb2.AdminMessage() + with patch("meshtastic.admin_pb2.AdminMessage", return_value=amesg): + with patch.object(anode, "_sendAdmin") as mock_send_admin: + test_hash = b"\x01\x02\x03" * 8 # 24 bytes hash + anode.startOTA(ota_mode=admin_pb2.OTAMode.OTA_WIFI, ota_file_hash=test_hash) + + # Verify the OTA request was set correctly + assert amesg.ota_request.reboot_ota_mode == admin_pb2.OTAMode.OTA_WIFI + assert amesg.ota_request.ota_hash == test_hash + mock_send_admin.assert_called_once_with(amesg) + + +@pytest.mark.unit +def test_start_ota_remote_node_raises_error(): + """Test startOTA on remote node raises ValueError""" + iface = MagicMock(autospec=MeshInterface) + local_node = Node(iface, 1234567890, noProto=True) + remote_node = Node(iface, 9876543210, noProto=True) + iface.localNode = local_node + + test_hash = b"\x01\x02\x03" * 8 + with pytest.raises(ValueError, match="startOTA only possible in local node"): + remote_node.startOTA( + ota_mode=admin_pb2.OTAMode.OTA_WIFI, ota_file_hash=test_hash + ) + + # TODO # @pytest.mark.unitslow # def test_waitForConfig(): diff --git a/meshtastic/tests/test_ota.py b/meshtastic/tests/test_ota.py new file mode 100644 index 000000000..870336122 --- /dev/null +++ b/meshtastic/tests/test_ota.py @@ -0,0 +1,455 @@ +"""Meshtastic unit tests for ota.py""" + +import hashlib +import logging +import os +import socket +import tempfile +from unittest.mock import MagicMock, patch + +import pytest + +from meshtastic.ota import ( + _file_sha256, + ESP32WiFiOTA, + OTAError, +) + + +@pytest.mark.unit +def test_file_sha256(): + """Test _file_sha256 calculates correct hash""" + # Create a temporary file with known content + with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f: + test_data = b"Hello, World!" + f.write(test_data) + temp_file = f.name + + try: + result = _file_sha256(temp_file) + expected_hash = hashlib.sha256(test_data).hexdigest() + assert result.hexdigest() == expected_hash + finally: + os.unlink(temp_file) + + +@pytest.mark.unit +def test_file_sha256_large_file(): + """Test _file_sha256 handles files larger than chunk size""" + # Create a temporary file with more than 4096 bytes + with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f: + test_data = b"A" * 8192 # More than 4096 bytes + f.write(test_data) + temp_file = f.name + + try: + result = _file_sha256(temp_file) + expected_hash = hashlib.sha256(test_data).hexdigest() + assert result.hexdigest() == expected_hash + finally: + os.unlink(temp_file) + + +@pytest.mark.unit +def test_esp32_wifi_ota_init_file_not_found(): + """Test ESP32WiFiOTA raises FileNotFoundError for non-existent file""" + with pytest.raises(FileNotFoundError, match="does not exist"): + ESP32WiFiOTA("/nonexistent/firmware.bin", "192.168.1.1") + + +@pytest.mark.unit +def test_esp32_wifi_ota_init_success(): + """Test ESP32WiFiOTA initializes correctly with valid file""" + with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f: + f.write(b"fake firmware data") + temp_file = f.name + + try: + ota = ESP32WiFiOTA(temp_file, "192.168.1.1", 3232) + assert ota._filename == temp_file + assert ota._hostname == "192.168.1.1" + assert ota._port == 3232 + assert ota._socket is None + # Verify hash is calculated + assert ota._file_hash is not None + assert len(ota.hash_hex()) == 64 # SHA256 hex is 64 chars + finally: + os.unlink(temp_file) + + +@pytest.mark.unit +def test_esp32_wifi_ota_init_default_port(): + """Test ESP32WiFiOTA uses default port 3232""" + with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f: + f.write(b"fake firmware data") + temp_file = f.name + + try: + ota = ESP32WiFiOTA(temp_file, "192.168.1.1") + assert ota._port == 3232 + finally: + os.unlink(temp_file) + + +@pytest.mark.unit +def test_esp32_wifi_ota_hash_bytes(): + """Test hash_bytes returns correct bytes""" + with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f: + test_data = b"firmware data" + f.write(test_data) + temp_file = f.name + + try: + ota = ESP32WiFiOTA(temp_file, "192.168.1.1") + hash_bytes = ota.hash_bytes() + expected_bytes = hashlib.sha256(test_data).digest() + assert hash_bytes == expected_bytes + assert len(hash_bytes) == 32 # SHA256 is 32 bytes + finally: + os.unlink(temp_file) + + +@pytest.mark.unit +def test_esp32_wifi_ota_hash_hex(): + """Test hash_hex returns correct hex string""" + with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f: + test_data = b"firmware data" + f.write(test_data) + temp_file = f.name + + try: + ota = ESP32WiFiOTA(temp_file, "192.168.1.1") + hash_hex = ota.hash_hex() + expected_hex = hashlib.sha256(test_data).hexdigest() + assert hash_hex == expected_hex + assert len(hash_hex) == 64 # SHA256 hex is 64 chars + finally: + os.unlink(temp_file) + + +@pytest.mark.unit +def test_esp32_wifi_ota_read_line_not_connected(): + """Test _read_line raises ConnectionError when not connected""" + with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f: + f.write(b"firmware") + temp_file = f.name + + try: + ota = ESP32WiFiOTA(temp_file, "192.168.1.1") + with pytest.raises(ConnectionError, match="Socket not connected"): + ota._read_line() + finally: + os.unlink(temp_file) + + +@pytest.mark.unit +def test_esp32_wifi_ota_read_line_connection_closed(): + """Test _read_line raises ConnectionError when connection closed""" + with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f: + f.write(b"firmware") + temp_file = f.name + + try: + ota = ESP32WiFiOTA(temp_file, "192.168.1.1") + mock_socket = MagicMock() + # Simulate connection closed + mock_socket.recv.return_value = b"" + ota._socket = mock_socket + + with pytest.raises(ConnectionError, match="Connection closed"): + ota._read_line() + finally: + os.unlink(temp_file) + + +@pytest.mark.unit +def test_esp32_wifi_ota_read_line_success(): + """Test _read_line successfully reads a line""" + with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f: + f.write(b"firmware") + temp_file = f.name + + try: + ota = ESP32WiFiOTA(temp_file, "192.168.1.1") + mock_socket = MagicMock() + # Simulate receiving "OK\n" + mock_socket.recv.side_effect = [b"O", b"K", b"\n"] + ota._socket = mock_socket + + result = ota._read_line() + assert result == "OK" + finally: + os.unlink(temp_file) + + +@pytest.mark.unit +@patch("meshtastic.ota.socket.socket") +def test_esp32_wifi_ota_update_success(mock_socket_class): + """Test update() with successful OTA""" + with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f: + test_data = b"A" * 1024 # 1KB of data + f.write(test_data) + temp_file = f.name + + try: + # Setup mock socket + mock_socket = MagicMock() + mock_socket_class.return_value = mock_socket + + ota = ESP32WiFiOTA(temp_file, "192.168.1.1") + + # Mock _read_line to return appropriate responses + # First call: ERASING, Second call: OK (ready), Third call: OK (complete) + with patch.object(ota, "_read_line") as mock_read_line: + mock_read_line.side_effect = [ + "ERASING", # Device is erasing flash + "OK", # Device ready for firmware + "OK", # Device finished successfully + ] + + ota.update() + + # Verify socket was created and connected + mock_socket_class.assert_called_once_with( + socket.AF_INET, socket.SOCK_STREAM + ) + mock_socket.settimeout.assert_called_once_with(15) + mock_socket.connect.assert_called_once_with(("192.168.1.1", 3232)) + + # Verify start command was sent + start_cmd = f"OTA {len(test_data)} {ota.hash_hex()}\n".encode("utf-8") + mock_socket.sendall.assert_any_call(start_cmd) + + # Verify firmware was sent (at least one chunk) + assert mock_socket.sendall.call_count >= 2 + + # Verify socket was closed + mock_socket.close.assert_called_once() + + finally: + os.unlink(temp_file) + + +@pytest.mark.unit +@patch("meshtastic.ota.socket.socket") +def test_esp32_wifi_ota_update_with_progress_callback(mock_socket_class): + """Test update() with progress callback""" + with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f: + test_data = b"A" * 1024 # 1KB of data + f.write(test_data) + temp_file = f.name + + try: + # Setup mock socket + mock_socket = MagicMock() + mock_socket_class.return_value = mock_socket + + ota = ESP32WiFiOTA(temp_file, "192.168.1.1") + + # Track progress callback calls + progress_calls = [] + + def progress_callback(sent, total): + progress_calls.append((sent, total)) + + # Mock _read_line + with patch.object(ota, "_read_line") as mock_read_line: + mock_read_line.side_effect = [ + "OK", # Device ready + "OK", # Device finished + ] + + ota.update(progress_callback=progress_callback) + + # Verify progress callback was called + assert len(progress_calls) > 0 + # First call should show some progress + assert progress_calls[0][0] > 0 + # Total should be the firmware size + assert progress_calls[0][1] == len(test_data) + # Last call should show all bytes sent + assert progress_calls[-1][0] == len(test_data) + + finally: + os.unlink(temp_file) + + +@pytest.mark.unit +@patch("meshtastic.ota.socket.socket") +def test_esp32_wifi_ota_update_device_error_on_start(mock_socket_class): + """Test update() raises OTAError when device reports error during start""" + with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f: + f.write(b"firmware") + temp_file = f.name + + try: + mock_socket = MagicMock() + mock_socket_class.return_value = mock_socket + + ota = ESP32WiFiOTA(temp_file, "192.168.1.1") + + with patch.object(ota, "_read_line") as mock_read_line: + mock_read_line.return_value = "ERR BAD_HASH" + + with pytest.raises(OTAError, match="Device reported error"): + ota.update() + + finally: + os.unlink(temp_file) + + +@pytest.mark.unit +@patch("meshtastic.ota.socket.socket") +def test_esp32_wifi_ota_update_device_error_on_finish(mock_socket_class): + """Test update() raises OTAError when device reports error after firmware sent""" + with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f: + f.write(b"firmware") + temp_file = f.name + + try: + mock_socket = MagicMock() + mock_socket_class.return_value = mock_socket + + ota = ESP32WiFiOTA(temp_file, "192.168.1.1") + + with patch.object(ota, "_read_line") as mock_read_line: + mock_read_line.side_effect = [ + "OK", # Device ready + "ERR FLASH_ERR", # Error after firmware sent + ] + + with pytest.raises(OTAError, match="OTA update failed"): + ota.update() + + finally: + os.unlink(temp_file) + + +@pytest.mark.unit +@patch("meshtastic.ota.socket.socket") +def test_esp32_wifi_ota_update_socket_cleanup_on_error(mock_socket_class): + """Test that socket is properly cleaned up on error""" + with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f: + f.write(b"firmware") + temp_file = f.name + + try: + mock_socket = MagicMock() + mock_socket_class.return_value = mock_socket + + ota = ESP32WiFiOTA(temp_file, "192.168.1.1") + + # Simulate connection error + mock_socket.connect.side_effect = ConnectionRefusedError("Connection refused") + + with pytest.raises(ConnectionRefusedError): + ota.update() + + # Verify socket was closed even on error + mock_socket.close.assert_called_once() + assert ota._socket is None + + finally: + os.unlink(temp_file) + + +@pytest.mark.unit +@patch("meshtastic.ota.socket.socket") +def test_esp32_wifi_ota_update_large_firmware(mock_socket_class): + """Test update() correctly chunks large firmware files""" + with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f: + # Create a file larger than chunk_size (1024) + test_data = b"B" * 3000 + f.write(test_data) + temp_file = f.name + + try: + mock_socket = MagicMock() + mock_socket_class.return_value = mock_socket + + ota = ESP32WiFiOTA(temp_file, "192.168.1.1") + + with patch.object(ota, "_read_line") as mock_read_line: + mock_read_line.side_effect = [ + "OK", # Device ready + "OK", # Device finished + ] + + ota.update() + + # Verify that all data was sent in chunks + # 3000 bytes should be sent in ~3 chunks of 1024 bytes + sendall_calls = [ + call + for call in mock_socket.sendall.call_args_list + if call[0][0] + != f"OTA {len(test_data)} {ota.hash_hex()}\n".encode("utf-8") + ] + # Calculate total data sent (excluding the start command) + total_sent = sum(len(call[0][0]) for call in sendall_calls) + assert total_sent == len(test_data) + + finally: + os.unlink(temp_file) + + +@pytest.mark.unit +@patch("meshtastic.ota.socket.socket") +def test_esp32_wifi_ota_update_unexpected_response_warning(mock_socket_class, caplog): + """Test update() logs warning on unexpected response during startup""" + with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f: + f.write(b"firmware") + temp_file = f.name + + try: + mock_socket = MagicMock() + mock_socket_class.return_value = mock_socket + + ota = ESP32WiFiOTA(temp_file, "192.168.1.1") + + with patch.object(ota, "_read_line") as mock_read_line: + mock_read_line.side_effect = [ + "UNKNOWN", # Unexpected response + "OK", # Then proceed + "OK", # Device finished + ] + + with caplog.at_level(logging.WARNING): + ota.update() + + # Check that warning was logged for unexpected response + assert "Unexpected response" in caplog.text + + finally: + os.unlink(temp_file) + + +@pytest.mark.unit +@patch("meshtastic.ota.socket.socket") +def test_esp32_wifi_ota_update_unexpected_final_response(mock_socket_class, caplog): + """Test update() logs warning on unexpected final response after firmware upload""" + with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f: + f.write(b"firmware") + temp_file = f.name + + try: + mock_socket = MagicMock() + mock_socket_class.return_value = mock_socket + + ota = ESP32WiFiOTA(temp_file, "192.168.1.1") + + with patch.object(ota, "_read_line") as mock_read_line: + mock_read_line.side_effect = [ + "OK", # Device ready for firmware + "UNKNOWN", # Unexpected final response (not OK, not ERR, not ACK) + "OK", # Then succeed + ] + + with caplog.at_level(logging.WARNING): + ota.update() + + # Check that warning was logged for unexpected final response + assert "Unexpected final response" in caplog.text + + finally: + os.unlink(temp_file) diff --git a/protobufs b/protobufs index 44298d374..e1a6b3a86 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 44298d374fd83cfbc36fdb76c6f966e980cadd93 +Subproject commit e1a6b3a868d735da72cd6c94c574d655129d390a diff --git a/pyproject.toml b/pyproject.toml index d195fef80..9f6cfae6c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "meshtastic" -version = "2.7.7" +version = "2.7.8" description = "Python API & client shell for talking to Meshtastic devices" authors = ["Meshtastic Developers "] license = "GPL-3.0-only"