From b7f8dd11a559f9cf7a427990e6d158d96a10a707 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Thu, 11 Sep 2025 21:44:43 +0200 Subject: [PATCH] SL: bugfix parameter handling in sunnylink restore and remote setting (#1234) * refactor: improve parameter handling in sunnylink for robustness - Updated `get_param_as_byte` to return `None` for nonexistent parameters. - Enhanced param compression and encoding in `sunnylinkd`. * refactor: centralize parameter restoration with new helper function - Added `save_param_from_base64_encoded_string` to handle param decoding and saving. - Updated backup manager and sunnylinkd to use the new method. - Improved code readability and reduced duplication in parameter handling logic. * don't bother * clean --- sunnypilot/sunnylink/athena/sunnylinkd.py | 26 +++++++--------- sunnypilot/sunnylink/backups/manager.py | 29 +++-------------- sunnypilot/sunnylink/utils.py | 38 +++++++++++++++++++++-- 3 files changed, 53 insertions(+), 40 deletions(-) diff --git a/sunnypilot/sunnylink/athena/sunnylinkd.py b/sunnypilot/sunnylink/athena/sunnylinkd.py index 363fa1defc..25a77c367b 100755 --- a/sunnypilot/sunnylink/athena/sunnylinkd.py +++ b/sunnypilot/sunnylink/athena/sunnylinkd.py @@ -23,7 +23,7 @@ from websocket import (ABNF, WebSocket, WebSocketException, WebSocketTimeoutExce import cereal.messaging as messaging from sunnypilot.sunnylink.api import SunnylinkApi -from sunnypilot.sunnylink.utils import sunnylink_need_register, sunnylink_ready, get_param_as_byte +from sunnypilot.sunnylink.utils import sunnylink_need_register, sunnylink_ready, get_param_as_byte, save_param_from_base64_encoded_string SUNNYLINK_ATHENA_HOST = os.getenv('SUNNYLINK_ATHENA_HOST', 'wss://ws.stg.api.sunnypilot.ai') HANDLER_THREADS = int(os.getenv('HANDLER_THREADS', "4")) @@ -184,14 +184,18 @@ def getParams(params_keys: list[str], compression: bool = False) -> str | dict[s try: param_keys_validated = [key for key in params_keys if key in getParamsAllKeys()] - params_dict: dict[str, list[dict[str, str | bool | int ]]] = {"params": [ - { + params_dict: dict[str, list[dict[str, str | bool | int]]] = {"params": []} + for key in param_keys_validated: + value = get_param_as_byte(key) + if value is None: + continue + + params_dict["params"].append({ "key": key, - "value": base64.b64encode(gzip.compress(get_param_as_byte(key)) if compression else get_param_as_byte(key)).decode('utf-8'), + "value": base64.b64encode(gzip.compress(value) if compression else value).decode('utf-8'), "type": int(params.get_type(key).value), "is_compressed": compression - } for key in param_keys_validated - ]} + }) response = {str(param.get('key')): str(param.get('value')) for param in params_dict.get("params", [])} response |= {"params": json.dumps(params_dict.get("params", []))} # Upcoming for settings v1 @@ -204,15 +208,9 @@ def getParams(params_keys: list[str], compression: bool = False) -> str | dict[s @dispatcher.add_method def saveParams(params_to_update: dict[str, str], compression: bool = False) -> None: - params = Params() - params_dict = {key: base64.b64decode(value) for key, value in params_to_update.items()} - - if compression: - params_dict = {key: gzip.decompress(value) for key, value in params_dict.items()} - - for key, value in params_dict.items(): + for key, value in params_to_update.items(): try: - params.put(key, value) + save_param_from_base64_encoded_string(key, value, compression) except Exception as e: cloudlog.error(f"sunnylinkd.saveParams.exception {e}") diff --git a/sunnypilot/sunnylink/backups/manager.py b/sunnypilot/sunnylink/backups/manager.py index 315300c73c..e52b547afe 100644 --- a/sunnypilot/sunnylink/backups/manager.py +++ b/sunnypilot/sunnylink/backups/manager.py @@ -12,7 +12,7 @@ from enum import Enum from typing import Any from openpilot.common.git import get_branch -from openpilot.common.params import Params, ParamKeyType, ParamKeyFlag +from openpilot.common.params import Params, ParamKeyFlag from openpilot.common.realtime import Ratekeeper from openpilot.common.swaglog import cloudlog from openpilot.system.version import get_version @@ -20,7 +20,7 @@ from openpilot.system.version import get_version from cereal import messaging, custom from sunnypilot.sunnylink.api import SunnylinkApi from sunnypilot.sunnylink.backups.utils import decrypt_compressed_data, encrypt_compress_data, SnakeCaseEncoder -from sunnypilot.sunnylink.utils import get_param_as_byte +from sunnypilot.sunnylink.utils import get_param_as_byte, save_param_from_base64_encoded_string class OperationType(Enum): @@ -173,8 +173,7 @@ class BackupManagerSP: self._update_progress(75.0, OperationType.RESTORE) # Apply configuration - all_values_encoded = self._get_metadata_value(backup_metadata, "all_values_encoded", "false") - self._apply_config(config_data, str(all_values_encoded).lower() == "true") + self._apply_config(config_data) self.restore_status = custom.BackupManagerSP.Status.completed self._update_progress(100.0, OperationType.RESTORE) @@ -187,7 +186,7 @@ class BackupManagerSP: self._report_status() return False - def _apply_config(self, config_data: dict[str, str], all_values_encoded: bool = False) -> None: + def _apply_config(self, config_data: dict[str, str]) -> None: """Applies configuration data from a backup, but only for parameters marked as backupable.""" backupable_params = [k.decode('utf-8') for k in self.params.all_keys(ParamKeyFlag.BACKUP)] backupable_set_lower = {p.lower() for p in backupable_params} @@ -199,26 +198,8 @@ class BackupManagerSP: if param.lower() in backupable_set_lower: # Find real param name (with correct casing) real_param = next(p for p in backupable_params if p.lower() == param.lower()) - param_type = self.params.get_type(real_param) try: - value = base64.b64decode(encoded_value) if all_values_encoded else encoded_value - - if param_type != ParamKeyType.BYTES: - value = value.decode('utf-8') # type: ignore - - if param_type == ParamKeyType.STRING: - value = value - elif param_type == ParamKeyType.BOOL: - value = value.lower() in ('true', '1', 'yes') # type: ignore - elif param_type == ParamKeyType.INT: - value = int(value) # type: ignore - elif param_type == ParamKeyType.FLOAT: - value = float(value) # type: ignore - elif param_type == ParamKeyType.TIME: - value = str(value) - elif param_type == ParamKeyType.JSON: - value = json.loads(value) - self.params.put(real_param, value) + save_param_from_base64_encoded_string(real_param, encoded_value) restored_count += 1 except Exception as e: cloudlog.error(f"Failed to restore param {param}: {str(e)}") diff --git a/sunnypilot/sunnylink/utils.py b/sunnypilot/sunnylink/utils.py index 569afd26b6..1310b91f0e 100644 --- a/sunnypilot/sunnylink/utils.py +++ b/sunnypilot/sunnylink/utils.py @@ -1,3 +1,5 @@ +import base64 +import gzip import json from sunnypilot.sunnylink.api import SunnylinkApi, UNREGISTERED_SUNNYLINK_DONGLE_ID from openpilot.common.params import Params, ParamKeyType @@ -58,13 +60,45 @@ def get_api_token(): print(f"API Token: {token}") -def get_param_as_byte(param_name: str) -> bytes: +def get_param_as_byte(param_name: str) -> bytes | None: + """Get a parameter as bytes. Returns None if the parameter does not exist.""" params = Params() param = params.get(param_name) - param_type = params.get_type(param_name) + if param is None: + return None + param_type = params.get_type(param_name) if param_type == ParamKeyType.BYTES: return bytes(param) elif param_type == ParamKeyType.JSON: return json.dumps(param).encode('utf-8') return str(param).encode('utf-8') + + +def save_param_from_base64_encoded_string(param_name: str, base64_encoded_data: str, is_compressed=False) -> None: + """Save a parameter from bytes. Overwrites the parameter if it already exists.""" + params = Params() + # Find real param name (with correct casing) + param_type = params.get_type(param_name) + value = base64.b64decode(base64_encoded_data) + + if is_compressed: + value = gzip.decompress(value) + + # We convert to string anything that isn't bytes first. We later transform further. + if param_type != ParamKeyType.BYTES: + value = value.decode('utf-8') # type: ignore + + if param_type == ParamKeyType.STRING: + value = value + elif param_type == ParamKeyType.BOOL: + value = value.lower() in ('true', '1', 'yes') # type: ignore + elif param_type == ParamKeyType.INT: + value = int(value) # type: ignore + elif param_type == ParamKeyType.FLOAT: + value = float(value) # type: ignore + elif param_type == ParamKeyType.TIME: + value = str(value) # type: ignore + elif param_type == ParamKeyType.JSON: + value = json.loads(value) + params.put(param_name, value)