mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 13:33:42 +08:00
104 lines
3.6 KiB
Python
104 lines
3.6 KiB
Python
"""Durable, reversible preparation of saved steering preferences."""
|
|
|
|
import base64
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import tempfile
|
|
from typing import Any, Literal
|
|
|
|
from openpilot.starpilot.saved_document import acquire_native_lock
|
|
from openpilot.starpilot.saved_source import read_saved
|
|
|
|
KEY = 'TuningPreparationState'
|
|
LIMIT = 65536
|
|
|
|
|
|
def encode_values(values):
|
|
return {key: None if value is None else base64.b64encode(value).decode() for key, value in values.items()}
|
|
|
|
|
|
def decode(raw, keys) -> dict[str, Any] | Literal[False] | None:
|
|
if raw is None:
|
|
return None
|
|
try:
|
|
value = json.loads(raw)
|
|
if type(value['version']) is not int or value['version'] != 1 or not isinstance(value['vehicle'], str):
|
|
return False
|
|
for field in ('prior', 'prepared'):
|
|
if not isinstance(value[field], dict) or set(value[field]) != set(keys):
|
|
return False
|
|
value[field] = {key: None if data is None else base64.b64decode(data, validate=True)
|
|
for key, data in value[field].items()}
|
|
return value
|
|
except (ValueError, TypeError, KeyError):
|
|
return False
|
|
|
|
|
|
def replace_saved(params, key, value):
|
|
destination = Path(params.get_param_path(key))
|
|
if value is None:
|
|
destination.unlink(missing_ok=True)
|
|
else:
|
|
with tempfile.NamedTemporaryFile(dir=destination.parent, delete=False) as staged:
|
|
temporary = Path(staged.name)
|
|
staged.write(value)
|
|
staged.flush()
|
|
os.fsync(staged.fileno())
|
|
try:
|
|
os.replace(temporary, destination)
|
|
finally:
|
|
temporary.unlink(missing_ok=True)
|
|
descriptor = os.open(destination.parent, os.O_RDONLY)
|
|
try:
|
|
os.fsync(descriptor)
|
|
finally:
|
|
os.close(descriptor)
|
|
|
|
|
|
def transition(params, vehicle, sources, desired, journal_raw, enabled, authorized):
|
|
"""Persist the snapshot before any tune changes; resume only owned values."""
|
|
root = Path(params.get_param_path(KEY)).parent.parent
|
|
try:
|
|
with (root / '.lock').open('a') as lock:
|
|
acquire_native_lock(lock.fileno())
|
|
if not authorized() or read_saved(params, KEY, LIMIT) != (journal_raw, True):
|
|
return False
|
|
journal = decode(journal_raw, sources)
|
|
if journal is False:
|
|
return False
|
|
if any(read_saved(params, key, LIMIT) != (value, True) for key, value in sources.items()):
|
|
return False
|
|
if journal is None:
|
|
if not enabled:
|
|
return True
|
|
created: dict[str, Any] = {'version': 1, 'vehicle': vehicle, 'prior': sources, 'prepared': desired}
|
|
journal = created
|
|
stored = {**journal, 'prior': encode_values(sources), 'prepared': encode_values(desired)}
|
|
replace_saved(params, KEY, json.dumps(stored, sort_keys=True, separators=(',', ':')).encode())
|
|
if journal['vehicle'] != vehicle:
|
|
return False
|
|
for key, current in sources.items():
|
|
if enabled and current not in (journal['prior'][key], journal['prepared'][key]):
|
|
return False
|
|
target = journal['prepared'] if enabled else journal['prior']
|
|
for key, value in target.items():
|
|
if not authorized():
|
|
return False
|
|
current, readable = read_saved(params, key, LIMIT)
|
|
if not readable or current != sources[key]:
|
|
return False
|
|
if current not in (journal['prior'][key], journal['prepared'][key]):
|
|
if enabled:
|
|
return False
|
|
continue
|
|
if current != value:
|
|
replace_saved(params, key, value)
|
|
if not authorized():
|
|
return False
|
|
if not enabled:
|
|
replace_saved(params, KEY, None)
|
|
return True
|
|
except (OSError, ValueError, TypeError):
|
|
return False
|