mirror of
https://github.com/dragonpilot/dragonpilot.git
synced 2026-08-23 09:13:47 +08:00
dragonpilot v0.10.2
This commit is contained in:
@@ -0,0 +1 @@
|
||||
eon/rat
|
||||
@@ -0,0 +1,16 @@
|
||||
import os
|
||||
from typing import cast
|
||||
|
||||
from openpilot.system.hardware.base import HardwareBase
|
||||
from openpilot.system.hardware.tici.hardware import Tici
|
||||
from openpilot.system.hardware.pc.hardware import Pc
|
||||
|
||||
TICI = os.path.isfile('/TICI')
|
||||
AGNOS = os.path.isfile('/AGNOS')
|
||||
PC = not TICI
|
||||
|
||||
|
||||
if TICI:
|
||||
HARDWARE = cast(HardwareBase, Tici())
|
||||
else:
|
||||
HARDWARE = cast(HardwareBase, Pc())
|
||||
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
#include "cereal/gen/cpp/log.capnp.h"
|
||||
|
||||
// no-op base hw class
|
||||
class HardwareNone {
|
||||
public:
|
||||
static std::string get_name() { return ""; }
|
||||
static cereal::InitData::DeviceType get_device_type() { return cereal::InitData::DeviceType::UNKNOWN; }
|
||||
static int get_voltage() { return 0; }
|
||||
static int get_current() { return 0; }
|
||||
|
||||
static std::string get_serial() { return "cccccc"; }
|
||||
|
||||
static std::map<std::string, std::string> get_init_logs() {
|
||||
return {};
|
||||
}
|
||||
|
||||
static void set_ir_power(int percentage) {}
|
||||
|
||||
static bool PC() { return false; }
|
||||
static bool TICI() { return false; }
|
||||
static bool AGNOS() { return false; }
|
||||
};
|
||||
@@ -0,0 +1,243 @@
|
||||
import os
|
||||
from abc import abstractmethod, ABC
|
||||
from dataclasses import dataclass, fields
|
||||
|
||||
from cereal import log
|
||||
|
||||
NetworkType = log.DeviceState.NetworkType
|
||||
|
||||
class LPAError(RuntimeError):
|
||||
pass
|
||||
|
||||
class LPAProfileNotFoundError(LPAError):
|
||||
pass
|
||||
|
||||
@dataclass
|
||||
class Profile:
|
||||
iccid: str
|
||||
nickname: str
|
||||
enabled: bool
|
||||
provider: str
|
||||
|
||||
@dataclass
|
||||
class ThermalZone:
|
||||
# a zone from /sys/class/thermal/thermal_zone*
|
||||
name: str # a.k.a type
|
||||
scale: float = 1000. # scale to get degrees in C
|
||||
zone_number = -1
|
||||
|
||||
def read(self) -> float:
|
||||
if self.zone_number < 0:
|
||||
for n in os.listdir("/sys/devices/virtual/thermal"):
|
||||
if not n.startswith("thermal_zone"):
|
||||
continue
|
||||
with open(os.path.join("/sys/devices/virtual/thermal", n, "type")) as f:
|
||||
if f.read().strip() == self.name:
|
||||
self.zone_number = int(n.removeprefix("thermal_zone"))
|
||||
break
|
||||
|
||||
try:
|
||||
with open(f"/sys/devices/virtual/thermal/thermal_zone{self.zone_number}/temp") as f:
|
||||
return int(f.read()) / self.scale
|
||||
except FileNotFoundError:
|
||||
return 0
|
||||
|
||||
@dataclass
|
||||
class ThermalConfig:
|
||||
cpu: list[ThermalZone] | None = None
|
||||
gpu: list[ThermalZone] | None = None
|
||||
dsp: ThermalZone | None = None
|
||||
pmic: list[ThermalZone] | None = None
|
||||
memory: ThermalZone | None = None
|
||||
intake: ThermalZone | None = None
|
||||
exhaust: ThermalZone | None = None
|
||||
case: ThermalZone | None = None
|
||||
|
||||
def get_msg(self):
|
||||
ret = {}
|
||||
for f in fields(ThermalConfig):
|
||||
v = getattr(self, f.name)
|
||||
if v is not None:
|
||||
if isinstance(v, list):
|
||||
ret[f.name + "TempC"] = [x.read() for x in v]
|
||||
else:
|
||||
ret[f.name + "TempC"] = v.read()
|
||||
return ret
|
||||
|
||||
class LPABase(ABC):
|
||||
@abstractmethod
|
||||
def bootstrap(self) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def list_profiles(self) -> list[Profile]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_active_profile(self) -> Profile | None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def delete_profile(self, iccid: str) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def download_profile(self, qr: str, nickname: str | None = None) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def nickname_profile(self, iccid: str, nickname: str) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def switch_profile(self, iccid: str) -> None:
|
||||
pass
|
||||
|
||||
def is_comma_profile(self, iccid: str) -> bool:
|
||||
return any(iccid.startswith(prefix) for prefix in ('8985235',))
|
||||
|
||||
class HardwareBase(ABC):
|
||||
@staticmethod
|
||||
def get_cmdline() -> dict[str, str]:
|
||||
with open('/proc/cmdline') as f:
|
||||
cmdline = f.read()
|
||||
return {kv[0]: kv[1] for kv in [s.split('=') for s in cmdline.split(' ')] if len(kv) == 2}
|
||||
|
||||
@staticmethod
|
||||
def read_param_file(path, parser, default=0):
|
||||
try:
|
||||
with open(path) as f:
|
||||
return parser(f.read())
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
def booted(self) -> bool:
|
||||
return True
|
||||
|
||||
@abstractmethod
|
||||
def reboot(self, reason=None):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def uninstall(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_os_version(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_device_type(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_imei(self, slot) -> str:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_serial(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_network_info(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_network_type(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_sim_info(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_sim_lpa(self) -> LPABase:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_network_strength(self, network_type):
|
||||
pass
|
||||
|
||||
def get_network_metered(self, network_type) -> bool:
|
||||
return network_type not in (NetworkType.none, NetworkType.wifi, NetworkType.ethernet)
|
||||
|
||||
@staticmethod
|
||||
def set_bandwidth_limit(upload_speed_kbps: int, download_speed_kbps: int) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_current_power_draw(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_som_power_draw(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def shutdown(self):
|
||||
pass
|
||||
|
||||
def get_thermal_config(self):
|
||||
return ThermalConfig()
|
||||
|
||||
def set_display_power(self, on: bool):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_screen_brightness(self, percentage):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_screen_brightness(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_power_save(self, powersave_enabled):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_gpu_usage_percent(self):
|
||||
pass
|
||||
|
||||
def get_modem_version(self):
|
||||
return None
|
||||
|
||||
@abstractmethod
|
||||
def get_modem_temperatures(self):
|
||||
pass
|
||||
|
||||
|
||||
@abstractmethod
|
||||
def initialize_hardware(self):
|
||||
pass
|
||||
|
||||
def configure_modem(self):
|
||||
pass
|
||||
|
||||
def reboot_modem(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_networks(self):
|
||||
pass
|
||||
|
||||
def has_internal_panda(self) -> bool:
|
||||
return False
|
||||
|
||||
def reset_internal_panda(self):
|
||||
pass
|
||||
|
||||
def recover_internal_panda(self):
|
||||
pass
|
||||
|
||||
def get_modem_data_usage(self):
|
||||
return -1, -1
|
||||
|
||||
def get_voltage(self) -> float:
|
||||
return 0.
|
||||
|
||||
def get_current(self) -> float:
|
||||
return 0.
|
||||
|
||||
def set_ir_power(self, percent: int):
|
||||
pass
|
||||
Executable
+70
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import time
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
from openpilot.system.hardware.base import LPABase
|
||||
|
||||
|
||||
def bootstrap(lpa: LPABase) -> None:
|
||||
print('┌──────────────────────────────────────────────────────────────────────────────┐')
|
||||
print('│ WARNING, PLEASE READ BEFORE PROCEEDING │')
|
||||
print('│ │')
|
||||
print('│ this is an irreversible operation that will remove the comma-provisioned │')
|
||||
print('│ profile. │')
|
||||
print('│ │')
|
||||
print('│ after this operation, you must purchase a new eSIM from comma in order to │')
|
||||
print('│ use the comma prime subscription again. │')
|
||||
print('└──────────────────────────────────────────────────────────────────────────────┘')
|
||||
print()
|
||||
for severity in ('sure', '100% sure'):
|
||||
print(f'are you {severity} you want to proceed? (y/N) ', end='')
|
||||
confirm = input()
|
||||
if confirm != 'y':
|
||||
print('aborting')
|
||||
exit(0)
|
||||
lpa.bootstrap()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(prog='esim.py', description='manage eSIM profiles on your comma device', epilog='comma.ai')
|
||||
parser.add_argument('--bootstrap', action='store_true', help='bootstrap the eUICC (required before downloading profiles)')
|
||||
parser.add_argument('--backend', choices=['qmi', 'at'], default='qmi', help='use the specified backend, defaults to qmi')
|
||||
parser.add_argument('--switch', metavar='iccid', help='switch to profile')
|
||||
parser.add_argument('--delete', metavar='iccid', help='delete profile (warning: this cannot be undone)')
|
||||
parser.add_argument('--download', nargs=2, metavar=('qr', 'name'), help='download a profile using QR code (format: LPA:1$rsp.truphone.com$QRF-SPEEDTEST)')
|
||||
parser.add_argument('--nickname', nargs=2, metavar=('iccid', 'name'), help='update the nickname for a profile')
|
||||
args = parser.parse_args()
|
||||
|
||||
mutated = False
|
||||
lpa = HARDWARE.get_sim_lpa()
|
||||
if args.bootstrap:
|
||||
bootstrap(lpa)
|
||||
mutated = True
|
||||
elif args.switch:
|
||||
lpa.switch_profile(args.switch)
|
||||
mutated = True
|
||||
elif args.delete:
|
||||
confirm = input('are you sure you want to delete this profile? (y/N) ')
|
||||
if confirm == 'y':
|
||||
lpa.delete_profile(args.delete)
|
||||
mutated = True
|
||||
else:
|
||||
print('cancelled')
|
||||
exit(0)
|
||||
elif args.download:
|
||||
lpa.download_profile(args.download[0], args.download[1])
|
||||
elif args.nickname:
|
||||
lpa.nickname_profile(args.nickname[0], args.nickname[1])
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
if mutated:
|
||||
HARDWARE.reboot_modem()
|
||||
# eUICC needs a small delay post-reboot before querying profiles
|
||||
time.sleep(.5)
|
||||
|
||||
profiles = lpa.list_profiles()
|
||||
print(f'\n{len(profiles)} profile{"s" if len(profiles) > 1 else ""}:')
|
||||
for p in profiles:
|
||||
print(f'- {p.iccid} (nickname: {p.nickname or "<none provided>"}) (provider: {p.provider}) - {"enabled" if p.enabled else "disabled"}')
|
||||
Executable
+38
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env python3
|
||||
import numpy as np
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from openpilot.common.realtime import DT_HW
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.common.pid import PIDController
|
||||
|
||||
class BaseFanController(ABC):
|
||||
@abstractmethod
|
||||
def update(self, cur_temp: float, ignition: bool) -> int:
|
||||
pass
|
||||
|
||||
|
||||
class TiciFanController(BaseFanController):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
cloudlog.info("Setting up TICI fan handler")
|
||||
|
||||
self.last_ignition = False
|
||||
self.controller = PIDController(k_p=0, k_i=4e-3, rate=(1 / DT_HW))
|
||||
|
||||
def update(self, cur_temp: float, ignition: bool) -> int:
|
||||
self.controller.pos_limit = 100 if ignition else 30
|
||||
self.controller.neg_limit = 30 if ignition else 0
|
||||
|
||||
if ignition != self.last_ignition:
|
||||
self.controller.reset()
|
||||
|
||||
error = cur_temp - 75
|
||||
fan_pwr_out = int(self.controller.update(
|
||||
error=error,
|
||||
feedforward=np.interp(cur_temp, [60.0, 100.0], [0, 100])
|
||||
))
|
||||
|
||||
self.last_ignition = ignition
|
||||
return fan_pwr_out
|
||||
|
||||
Executable
+501
@@ -0,0 +1,501 @@
|
||||
#!/usr/bin/env python3
|
||||
import fcntl
|
||||
import os
|
||||
import queue
|
||||
import struct
|
||||
import threading
|
||||
import time
|
||||
from collections import OrderedDict, namedtuple
|
||||
|
||||
import psutil
|
||||
|
||||
import cereal.messaging as messaging
|
||||
from cereal import log
|
||||
from cereal.services import SERVICE_LIST
|
||||
from openpilot.common.utils import strip_deprecated_keys
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.realtime import DT_HW
|
||||
from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert
|
||||
from openpilot.system.hardware import HARDWARE, TICI, AGNOS, PC
|
||||
from openpilot.system.loggerd.config import get_available_percent
|
||||
from openpilot.system.statsd import statlog
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.system.hardware.power_monitoring import PowerMonitoring
|
||||
from openpilot.system.hardware.fan_controller import TiciFanController
|
||||
from openpilot.system.version import terms_version, training_version
|
||||
from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID
|
||||
|
||||
ThermalStatus = log.DeviceState.ThermalStatus
|
||||
NetworkType = log.DeviceState.NetworkType
|
||||
NetworkStrength = log.DeviceState.NetworkStrength
|
||||
CURRENT_TAU = 15. # 15s time constant
|
||||
TEMP_TAU = 5. # 5s time constant
|
||||
DISCONNECT_TIMEOUT = 5. # wait 5 seconds before going offroad after disconnect so you get an alert
|
||||
PANDA_STATES_TIMEOUT = round(1000 / SERVICE_LIST['pandaStates'].frequency * 1.5) # 1.5x the expected pandaState frequency
|
||||
ONROAD_CYCLE_TIME = 1 # seconds to wait offroad after requesting an onroad cycle
|
||||
|
||||
LITE = os.getenv("LITE")
|
||||
|
||||
ThermalBand = namedtuple("ThermalBand", ['min_temp', 'max_temp'])
|
||||
HardwareState = namedtuple("HardwareState", ['network_type', 'network_info', 'network_strength', 'network_stats',
|
||||
'network_metered', 'modem_temps'])
|
||||
|
||||
# List of thermal bands. We will stay within this region as long as we are within the bounds.
|
||||
# When exiting the bounds, we'll jump to the lower or higher band. Bands are ordered in the dict.
|
||||
THERMAL_BANDS = OrderedDict({
|
||||
ThermalStatus.green: ThermalBand(None, 80.0),
|
||||
ThermalStatus.yellow: ThermalBand(75.0, 96.0),
|
||||
ThermalStatus.red: ThermalBand(88.0, 107.),
|
||||
ThermalStatus.danger: ThermalBand(94.0, None),
|
||||
})
|
||||
|
||||
# Override to highest thermal band when offroad and above this temp
|
||||
OFFROAD_DANGER_TEMP = 75
|
||||
|
||||
prev_offroad_states: dict[str, tuple[bool, str | None]] = {}
|
||||
|
||||
|
||||
|
||||
def set_offroad_alert_if_changed(offroad_alert: str, show_alert: bool, extra_text: str | None=None):
|
||||
if prev_offroad_states.get(offroad_alert, None) == (show_alert, extra_text):
|
||||
return
|
||||
prev_offroad_states[offroad_alert] = (show_alert, extra_text)
|
||||
set_offroad_alert(offroad_alert, show_alert, extra_text)
|
||||
|
||||
def touch_thread(end_event):
|
||||
count = 0
|
||||
|
||||
pm = messaging.PubMaster(["touch"])
|
||||
|
||||
event_format = "llHHi"
|
||||
event_size = struct.calcsize(event_format)
|
||||
event_frame = []
|
||||
|
||||
with open("/dev/input/by-path/platform-894000.i2c-event", "rb") as event_file:
|
||||
fcntl.fcntl(event_file, fcntl.F_SETFL, os.O_NONBLOCK)
|
||||
while not end_event.is_set():
|
||||
if (count % int(1. / DT_HW)) == 0:
|
||||
event = event_file.read(event_size)
|
||||
if event:
|
||||
(sec, usec, etype, code, value) = struct.unpack(event_format, event)
|
||||
if etype != 0 or code != 0 or value != 0:
|
||||
touch = log.Touch.new_message()
|
||||
touch.sec = sec
|
||||
touch.usec = usec
|
||||
touch.type = etype
|
||||
touch.code = code
|
||||
touch.value = value
|
||||
event_frame.append(touch)
|
||||
else: # end of frame, push new log
|
||||
msg = messaging.new_message('touch', len(event_frame), valid=True)
|
||||
msg.touch = event_frame
|
||||
pm.send('touch', msg)
|
||||
event_frame = []
|
||||
continue
|
||||
|
||||
count += 1
|
||||
time.sleep(DT_HW)
|
||||
|
||||
|
||||
def hw_state_thread(end_event, hw_queue):
|
||||
"""Handles non critical hardware state, and sends over queue"""
|
||||
count = 0
|
||||
prev_hw_state = None
|
||||
|
||||
modem_version = None
|
||||
modem_configured = False
|
||||
modem_missing_count = 0
|
||||
modem_restart_count = 0
|
||||
|
||||
while not end_event.is_set():
|
||||
# these are expensive calls. update every 10s
|
||||
if (count % int(10. / DT_HW)) == 0:
|
||||
try:
|
||||
network_type = HARDWARE.get_network_type()
|
||||
modem_temps = HARDWARE.get_modem_temperatures()
|
||||
if len(modem_temps) == 0 and prev_hw_state is not None:
|
||||
modem_temps = prev_hw_state.modem_temps
|
||||
|
||||
# Log modem version once
|
||||
if AGNOS and (modem_version is None):
|
||||
modem_version = HARDWARE.get_modem_version()
|
||||
|
||||
if modem_version is not None:
|
||||
cloudlog.event("modem version", version=modem_version)
|
||||
|
||||
if AGNOS and modem_restart_count < 3 and HARDWARE.get_modem_version() is None:
|
||||
# TODO: we may be able to remove this with a MM update
|
||||
# ModemManager's probing on startup can fail
|
||||
# rarely, restart the service to probe again.
|
||||
# Also, AT commands sometimes timeout resulting in ModemManager not
|
||||
# trying to use this modem anymore.
|
||||
modem_missing_count += 1
|
||||
if (modem_missing_count % 4) == 0:
|
||||
modem_restart_count += 1
|
||||
cloudlog.event("restarting ModemManager")
|
||||
os.system("sudo systemctl restart --no-block ModemManager")
|
||||
|
||||
tx, rx = HARDWARE.get_modem_data_usage()
|
||||
|
||||
hw_state = HardwareState(
|
||||
network_type=network_type,
|
||||
network_info=HARDWARE.get_network_info(),
|
||||
network_strength=HARDWARE.get_network_strength(network_type),
|
||||
network_stats={'wwanTx': tx, 'wwanRx': rx},
|
||||
network_metered=HARDWARE.get_network_metered(network_type),
|
||||
modem_temps=modem_temps,
|
||||
)
|
||||
|
||||
try:
|
||||
hw_queue.put_nowait(hw_state)
|
||||
except queue.Full:
|
||||
pass
|
||||
|
||||
if not modem_configured and HARDWARE.get_modem_version() is not None:
|
||||
cloudlog.warning("configuring modem")
|
||||
HARDWARE.configure_modem()
|
||||
modem_configured = True
|
||||
|
||||
prev_hw_state = hw_state
|
||||
except Exception:
|
||||
cloudlog.exception("Error getting hardware state")
|
||||
|
||||
count += 1
|
||||
time.sleep(DT_HW)
|
||||
|
||||
|
||||
def hardware_thread(end_event, hw_queue) -> None:
|
||||
pm = messaging.PubMaster(['deviceState'])
|
||||
sm = messaging.SubMaster(["peripheralState", "gpsLocationExternal", "selfdriveState", "pandaStates"], poll="pandaStates")
|
||||
|
||||
count = 0
|
||||
|
||||
onroad_conditions: dict[str, bool] = {
|
||||
"ignition": False,
|
||||
"not_onroad_cycle": True,
|
||||
"device_temp_good": True,
|
||||
}
|
||||
startup_conditions: dict[str, bool] = {}
|
||||
startup_conditions_prev: dict[str, bool] = {}
|
||||
|
||||
off_ts: float | None = None
|
||||
started_ts: float | None = None
|
||||
started_seen = False
|
||||
startup_blocked_ts: float | None = None
|
||||
thermal_status = ThermalStatus.yellow
|
||||
|
||||
last_hw_state = HardwareState(
|
||||
network_type=NetworkType.none,
|
||||
network_info=None,
|
||||
network_metered=False,
|
||||
network_strength=NetworkStrength.unknown,
|
||||
network_stats={'wwanTx': -1, 'wwanRx': -1},
|
||||
modem_temps=[],
|
||||
)
|
||||
|
||||
all_temp_filter = FirstOrderFilter(0., TEMP_TAU, DT_HW, initialized=False)
|
||||
offroad_temp_filter = FirstOrderFilter(0., TEMP_TAU, DT_HW, initialized=False)
|
||||
should_start_prev = False
|
||||
in_car = False
|
||||
engaged_prev = False
|
||||
pwrsave = False
|
||||
offroad_cycle_count = 0
|
||||
|
||||
params = Params()
|
||||
power_monitor = PowerMonitoring()
|
||||
|
||||
uptime_offroad: float = params.get("UptimeOffroad", return_default=True)
|
||||
uptime_onroad: float = params.get("UptimeOnroad", return_default=True)
|
||||
last_uptime_ts: float = time.monotonic()
|
||||
|
||||
HARDWARE.initialize_hardware()
|
||||
thermal_config = HARDWARE.get_thermal_config()
|
||||
|
||||
fan_controller = None
|
||||
|
||||
dp_dev_go_off_road = False
|
||||
|
||||
while not end_event.is_set():
|
||||
sm.update(PANDA_STATES_TIMEOUT)
|
||||
|
||||
pandaStates = sm['pandaStates']
|
||||
peripheralState = sm['peripheralState']
|
||||
peripheral_panda_present = peripheralState.pandaType != log.PandaState.PandaType.unknown
|
||||
|
||||
# handle requests to cycle system started state
|
||||
if params.get_bool("OnroadCycleRequested"):
|
||||
params.put_bool("OnroadCycleRequested", False)
|
||||
offroad_cycle_count = sm.frame
|
||||
onroad_conditions["not_onroad_cycle"] = (sm.frame - offroad_cycle_count) >= ONROAD_CYCLE_TIME * SERVICE_LIST['pandaStates'].frequency
|
||||
|
||||
if sm.updated['pandaStates'] and len(pandaStates) > 0:
|
||||
|
||||
# Set ignition based on any panda connected
|
||||
onroad_conditions["ignition"] = any(ps.ignitionLine or ps.ignitionCan for ps in pandaStates if ps.pandaType != log.PandaState.PandaType.unknown)
|
||||
|
||||
pandaState = pandaStates[0]
|
||||
|
||||
in_car = pandaState.harnessStatus != log.PandaState.HarnessStatus.notConnected
|
||||
|
||||
# Setup fan handler on first connect to panda
|
||||
if fan_controller is None and peripheral_panda_present:
|
||||
if TICI:
|
||||
fan_controller = TiciFanController()
|
||||
|
||||
elif (time.monotonic() - sm.recv_time['pandaStates']) > DISCONNECT_TIMEOUT:
|
||||
if onroad_conditions["ignition"]:
|
||||
onroad_conditions["ignition"] = False
|
||||
cloudlog.error("panda timed out onroad")
|
||||
|
||||
# Run at 2Hz, plus either edge of ignition
|
||||
ign_edge = (started_ts is not None) != all(onroad_conditions.values())
|
||||
if (sm.frame % round(SERVICE_LIST['pandaStates'].frequency * DT_HW) != 0) and not ign_edge:
|
||||
continue
|
||||
|
||||
msg = messaging.new_message('deviceState', valid=True)
|
||||
msg.deviceState = thermal_config.get_msg()
|
||||
msg.deviceState.deviceType = HARDWARE.get_device_type()
|
||||
|
||||
try:
|
||||
last_hw_state = hw_queue.get_nowait()
|
||||
except queue.Empty:
|
||||
pass
|
||||
|
||||
msg.deviceState.freeSpacePercent = get_available_percent(default=100.0)
|
||||
msg.deviceState.memoryUsagePercent = int(round(psutil.virtual_memory().percent))
|
||||
msg.deviceState.gpuUsagePercent = int(round(HARDWARE.get_gpu_usage_percent()))
|
||||
online_cpu_usage = [int(round(n)) for n in psutil.cpu_percent(percpu=True)]
|
||||
offline_cpu_usage = [0., ] * (len(msg.deviceState.cpuTempC) - len(online_cpu_usage))
|
||||
msg.deviceState.cpuUsagePercent = online_cpu_usage + offline_cpu_usage
|
||||
|
||||
msg.deviceState.networkType = last_hw_state.network_type
|
||||
msg.deviceState.networkMetered = last_hw_state.network_metered
|
||||
msg.deviceState.networkStrength = last_hw_state.network_strength
|
||||
msg.deviceState.networkStats = last_hw_state.network_stats
|
||||
if last_hw_state.network_info is not None:
|
||||
msg.deviceState.networkInfo = last_hw_state.network_info
|
||||
|
||||
msg.deviceState.modemTempC = last_hw_state.modem_temps
|
||||
|
||||
msg.deviceState.screenBrightnessPercent = HARDWARE.get_screen_brightness()
|
||||
|
||||
# this subset is only used for offroad
|
||||
temp_sources = [
|
||||
msg.deviceState.memoryTempC,
|
||||
max(msg.deviceState.cpuTempC, default=0.),
|
||||
max(msg.deviceState.gpuTempC, default=0.),
|
||||
]
|
||||
offroad_comp_temp = offroad_temp_filter.update(max(temp_sources))
|
||||
|
||||
# this drives the thermal status while onroad
|
||||
temp_sources.append(max(msg.deviceState.pmicTempC, default=0.))
|
||||
all_comp_temp = all_temp_filter.update(max(temp_sources))
|
||||
msg.deviceState.maxTempC = all_comp_temp
|
||||
|
||||
if fan_controller is not None:
|
||||
msg.deviceState.fanSpeedPercentDesired = fan_controller.update(all_comp_temp, onroad_conditions["ignition"])
|
||||
|
||||
is_offroad_for_5_min = (started_ts is None) and ((not started_seen) or (off_ts is None) or (time.monotonic() - off_ts > 60 * 5))
|
||||
if is_offroad_for_5_min and offroad_comp_temp > OFFROAD_DANGER_TEMP:
|
||||
# if device is offroad and already hot without the extra onroad load,
|
||||
# we want to cool down first before increasing load
|
||||
thermal_status = ThermalStatus.danger
|
||||
else:
|
||||
current_band = THERMAL_BANDS[thermal_status]
|
||||
band_idx = list(THERMAL_BANDS.keys()).index(thermal_status)
|
||||
if current_band.min_temp is not None and all_comp_temp < current_band.min_temp:
|
||||
thermal_status = list(THERMAL_BANDS.keys())[band_idx - 1]
|
||||
elif current_band.max_temp is not None and all_comp_temp > current_band.max_temp:
|
||||
thermal_status = list(THERMAL_BANDS.keys())[band_idx + 1]
|
||||
|
||||
# **** starting logic ****
|
||||
|
||||
startup_conditions["up_to_date"] = params.get("Offroad_ConnectivityNeeded") is None or params.get_bool("DisableUpdates") or params.get_bool("SnoozeUpdate")
|
||||
startup_conditions["no_excessive_actuation"] = params.get("Offroad_ExcessiveActuation") is None
|
||||
startup_conditions["not_uninstalling"] = not params.get_bool("DoUninstall")
|
||||
startup_conditions["accepted_terms"] = params.get("HasAcceptedTerms") == terms_version
|
||||
|
||||
# with 2% left, we killall, otherwise the phone will take a long time to boot
|
||||
startup_conditions["free_space"] = msg.deviceState.freeSpacePercent > 2
|
||||
startup_conditions["completed_training"] = params.get("CompletedTrainingVersion") == training_version
|
||||
startup_conditions["not_driver_view"] = not params.get_bool("IsDriverViewEnabled")
|
||||
startup_conditions["not_taking_snapshot"] = not params.get_bool("IsTakingSnapshot")
|
||||
|
||||
# must be at an engageable thermal band to go onroad
|
||||
startup_conditions["device_temp_engageable"] = thermal_status < ThermalStatus.red
|
||||
|
||||
# ensure device is fully booted
|
||||
startup_conditions["device_booted"] = startup_conditions.get("device_booted", False) or HARDWARE.booted()
|
||||
|
||||
# if the temperature enters the danger zone, go offroad to cool down
|
||||
onroad_conditions["device_temp_good"] = thermal_status < ThermalStatus.danger
|
||||
extra_text = f"{offroad_comp_temp:.1f}C"
|
||||
show_alert = (not onroad_conditions["device_temp_good"] or not startup_conditions["device_temp_engageable"]) and onroad_conditions["ignition"]
|
||||
set_offroad_alert_if_changed("Offroad_TemperatureTooHigh", show_alert, extra_text=extra_text)
|
||||
|
||||
# *** registration check ***
|
||||
# if not PC:
|
||||
# we enforce this for our software, but you are welcome
|
||||
# to make a different decision in your software
|
||||
# startup_conditions["registered_device"] = PC or (params.get("DongleId") != UNREGISTERED_DONGLE_ID)
|
||||
|
||||
# Handle offroad/onroad transition
|
||||
if count % 6 == 0:
|
||||
dp_dev_go_off_road = params.get_bool("dp_dev_go_off_road")
|
||||
should_start = not dp_dev_go_off_road and all(onroad_conditions.values())
|
||||
if started_ts is None:
|
||||
should_start = should_start and all(startup_conditions.values())
|
||||
|
||||
if should_start != should_start_prev or (count == 0):
|
||||
params.put_bool("IsEngaged", False)
|
||||
engaged_prev = False
|
||||
|
||||
if sm.updated['selfdriveState']:
|
||||
engaged = sm['selfdriveState'].enabled
|
||||
if engaged != engaged_prev:
|
||||
params.put_bool("IsEngaged", engaged)
|
||||
engaged_prev = engaged
|
||||
|
||||
try:
|
||||
with open('/dev/kmsg', 'w') as kmsg:
|
||||
kmsg.write(f"<3>[hardware] engaged: {engaged}\n")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
should_pwrsave = not onroad_conditions["ignition"] and msg.deviceState.screenBrightnessPercent < 1e-3
|
||||
if should_pwrsave != pwrsave or (count == 0):
|
||||
HARDWARE.set_power_save(should_pwrsave)
|
||||
pwrsave = should_pwrsave
|
||||
|
||||
if should_start:
|
||||
off_ts = None
|
||||
if started_ts is None:
|
||||
started_ts = time.monotonic()
|
||||
started_seen = True
|
||||
if startup_blocked_ts is not None:
|
||||
cloudlog.event("Startup after block", block_duration=(time.monotonic() - startup_blocked_ts),
|
||||
startup_conditions=startup_conditions, onroad_conditions=onroad_conditions,
|
||||
startup_conditions_prev=startup_conditions_prev, error=True)
|
||||
startup_blocked_ts = None
|
||||
else:
|
||||
if onroad_conditions["ignition"] and (startup_conditions != startup_conditions_prev):
|
||||
cloudlog.event("Startup blocked", startup_conditions=startup_conditions, onroad_conditions=onroad_conditions, error=True)
|
||||
startup_conditions_prev = startup_conditions.copy()
|
||||
startup_blocked_ts = time.monotonic()
|
||||
|
||||
started_ts = None
|
||||
if off_ts is None:
|
||||
off_ts = time.monotonic()
|
||||
|
||||
# Offroad power monitoring
|
||||
voltage = None if peripheralState.pandaType == log.PandaState.PandaType.unknown else peripheralState.voltage
|
||||
power_monitor.calculate(voltage, onroad_conditions["ignition"])
|
||||
msg.deviceState.offroadPowerUsageUwh = power_monitor.get_power_used()
|
||||
msg.deviceState.carBatteryCapacityUwh = max(0, power_monitor.get_car_battery_capacity())
|
||||
current_power_draw = HARDWARE.get_current_power_draw()
|
||||
statlog.sample("power_draw", current_power_draw)
|
||||
msg.deviceState.powerDrawW = current_power_draw
|
||||
|
||||
som_power_draw = HARDWARE.get_som_power_draw()
|
||||
statlog.sample("som_power_draw", som_power_draw)
|
||||
msg.deviceState.somPowerDrawW = som_power_draw
|
||||
|
||||
# Check if we need to shut down
|
||||
if power_monitor.should_shutdown(onroad_conditions["ignition"], in_car, off_ts, started_seen):
|
||||
cloudlog.warning(f"shutting device down, offroad since {off_ts}")
|
||||
params.put_bool("DoShutdown", True)
|
||||
|
||||
msg.deviceState.started = started_ts is not None
|
||||
msg.deviceState.startedMonoTime = int(1e9*(started_ts or 0))
|
||||
|
||||
last_ping = params.get("LastAthenaPingTime")
|
||||
if last_ping is not None:
|
||||
msg.deviceState.lastAthenaPingTime = last_ping
|
||||
|
||||
msg.deviceState.thermalStatus = thermal_status
|
||||
pm.send("deviceState", msg)
|
||||
|
||||
# Log to statsd
|
||||
statlog.gauge("free_space_percent", msg.deviceState.freeSpacePercent)
|
||||
statlog.gauge("gpu_usage_percent", msg.deviceState.gpuUsagePercent)
|
||||
statlog.gauge("memory_usage_percent", msg.deviceState.memoryUsagePercent)
|
||||
for i, usage in enumerate(msg.deviceState.cpuUsagePercent):
|
||||
statlog.gauge(f"cpu{i}_usage_percent", usage)
|
||||
for i, temp in enumerate(msg.deviceState.cpuTempC):
|
||||
statlog.gauge(f"cpu{i}_temperature", temp)
|
||||
for i, temp in enumerate(msg.deviceState.gpuTempC):
|
||||
statlog.gauge(f"gpu{i}_temperature", temp)
|
||||
statlog.gauge("memory_temperature", msg.deviceState.memoryTempC)
|
||||
for i, temp in enumerate(msg.deviceState.pmicTempC):
|
||||
statlog.gauge(f"pmic{i}_temperature", temp)
|
||||
for i, temp in enumerate(last_hw_state.modem_temps):
|
||||
statlog.gauge(f"modem_temperature{i}", temp)
|
||||
statlog.gauge("fan_speed_percent_desired", msg.deviceState.fanSpeedPercentDesired)
|
||||
statlog.gauge("screen_brightness_percent", msg.deviceState.screenBrightnessPercent)
|
||||
|
||||
# report to server once every 10 minutes
|
||||
rising_edge_started = should_start and not should_start_prev
|
||||
if rising_edge_started or (count % int(600. / DT_HW)) == 0:
|
||||
dat = {
|
||||
'count': count,
|
||||
'pandaStates': [strip_deprecated_keys(p.to_dict()) for p in pandaStates],
|
||||
'peripheralState': strip_deprecated_keys(peripheralState.to_dict()),
|
||||
'location': (strip_deprecated_keys(sm["gpsLocationExternal"].to_dict()) if sm.alive["gpsLocationExternal"] else None),
|
||||
'deviceState': strip_deprecated_keys(msg.to_dict())
|
||||
}
|
||||
cloudlog.event("STATUS_PACKET", **dat)
|
||||
|
||||
# save last one before going onroad
|
||||
if rising_edge_started:
|
||||
try:
|
||||
params.put("LastOffroadStatusPacket", dat)
|
||||
except Exception:
|
||||
cloudlog.exception("failed to save offroad status")
|
||||
|
||||
params.put_bool_nonblocking("NetworkMetered", msg.deviceState.networkMetered)
|
||||
|
||||
now_ts = time.monotonic()
|
||||
if off_ts:
|
||||
uptime_offroad += now_ts - max(last_uptime_ts, off_ts)
|
||||
elif started_ts:
|
||||
uptime_onroad += now_ts - max(last_uptime_ts, started_ts)
|
||||
last_uptime_ts = now_ts
|
||||
|
||||
if (count % int(60. / DT_HW)) == 0:
|
||||
params.put("UptimeOffroad", uptime_offroad)
|
||||
params.put("UptimeOnroad", uptime_onroad)
|
||||
|
||||
count += 1
|
||||
should_start_prev = should_start
|
||||
|
||||
|
||||
def main():
|
||||
hw_queue = queue.Queue(maxsize=1)
|
||||
end_event = threading.Event()
|
||||
|
||||
threads = [
|
||||
threading.Thread(target=hw_state_thread, args=(end_event, hw_queue)),
|
||||
threading.Thread(target=hardware_thread, args=(end_event, hw_queue)),
|
||||
]
|
||||
|
||||
if TICI:
|
||||
threads.append(threading.Thread(target=touch_thread, args=(end_event,)))
|
||||
|
||||
for t in threads:
|
||||
t.start()
|
||||
|
||||
try:
|
||||
while True:
|
||||
time.sleep(1)
|
||||
if not all(t.is_alive() for t in threads):
|
||||
break
|
||||
finally:
|
||||
end_event.set()
|
||||
|
||||
for t in threads:
|
||||
t.join()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,58 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "system/hardware/base.h"
|
||||
#include "common/util.h"
|
||||
|
||||
#if __TICI__
|
||||
#include "system/hardware/tici/hardware.h"
|
||||
#define Hardware HardwareTici
|
||||
#else
|
||||
#include "system/hardware/pc/hardware.h"
|
||||
#define Hardware HardwarePC
|
||||
#endif
|
||||
|
||||
namespace Path {
|
||||
inline std::string openpilot_prefix() {
|
||||
return util::getenv("OPENPILOT_PREFIX", "");
|
||||
}
|
||||
|
||||
inline std::string comma_home() {
|
||||
return util::getenv("HOME") + "/.comma" + Path::openpilot_prefix();
|
||||
}
|
||||
|
||||
inline std::string log_root() {
|
||||
if (const char *env = getenv("LOG_ROOT")) {
|
||||
return env;
|
||||
}
|
||||
return Hardware::PC() ? Path::comma_home() + "/media/0/realdata" : "/data/media/0/realdata";
|
||||
}
|
||||
|
||||
inline std::string params() {
|
||||
return util::getenv("PARAMS_ROOT", Hardware::PC() ? (Path::comma_home() + "/params") : "/data/params");
|
||||
}
|
||||
|
||||
inline std::string rsa_file() {
|
||||
return Hardware::PC() ? Path::comma_home() + "/persist/comma/id_rsa" : "/persist/comma/id_rsa";
|
||||
}
|
||||
|
||||
inline std::string swaglog_ipc() {
|
||||
return "ipc:///tmp/logmessage" + Path::openpilot_prefix();
|
||||
}
|
||||
|
||||
inline std::string download_cache_root() {
|
||||
if (const char *env = getenv("COMMA_CACHE")) {
|
||||
return env;
|
||||
}
|
||||
return "/tmp/comma_download_cache" + Path::openpilot_prefix() + "/";
|
||||
}
|
||||
|
||||
inline std::string shm_path() {
|
||||
#ifdef __APPLE__
|
||||
return"/tmp";
|
||||
#else
|
||||
return "/dev/shm";
|
||||
#endif
|
||||
}
|
||||
} // namespace Path
|
||||
@@ -0,0 +1,65 @@
|
||||
import os
|
||||
import platform
|
||||
from pathlib import Path
|
||||
|
||||
from openpilot.system.hardware import PC
|
||||
|
||||
DEFAULT_DOWNLOAD_CACHE_ROOT = "/tmp/comma_download_cache"
|
||||
|
||||
class Paths:
|
||||
@staticmethod
|
||||
def comma_home() -> str:
|
||||
return os.path.join(str(Path.home()), ".comma" + os.environ.get("OPENPILOT_PREFIX", ""))
|
||||
|
||||
@staticmethod
|
||||
def log_root() -> str:
|
||||
if os.environ.get('LOG_ROOT', False):
|
||||
return os.environ['LOG_ROOT']
|
||||
elif PC:
|
||||
return str(Path(Paths.comma_home()) / "media" / "0" / "realdata")
|
||||
else:
|
||||
return '/data/media/0/realdata/'
|
||||
|
||||
@staticmethod
|
||||
def swaglog_root() -> str:
|
||||
if PC:
|
||||
return os.path.join(Paths.comma_home(), "log")
|
||||
else:
|
||||
return "/data/log/"
|
||||
|
||||
@staticmethod
|
||||
def swaglog_ipc() -> str:
|
||||
return "ipc:///tmp/logmessage" + os.environ.get("OPENPILOT_PREFIX", "")
|
||||
|
||||
@staticmethod
|
||||
def download_cache_root() -> str:
|
||||
if os.environ.get('COMMA_CACHE', False):
|
||||
return os.environ['COMMA_CACHE'] + "/"
|
||||
return DEFAULT_DOWNLOAD_CACHE_ROOT + os.environ.get("OPENPILOT_PREFIX", "") + "/"
|
||||
|
||||
@staticmethod
|
||||
def persist_root() -> str:
|
||||
if PC:
|
||||
return os.path.join(Paths.comma_home(), "persist")
|
||||
else:
|
||||
return "/persist/"
|
||||
|
||||
@staticmethod
|
||||
def stats_root() -> str:
|
||||
if PC:
|
||||
return str(Path(Paths.comma_home()) / "stats")
|
||||
else:
|
||||
return "/data/stats/"
|
||||
|
||||
@staticmethod
|
||||
def config_root() -> str:
|
||||
if PC:
|
||||
return Paths.comma_home()
|
||||
else:
|
||||
return "/tmp/.comma"
|
||||
|
||||
@staticmethod
|
||||
def shm_path() -> str:
|
||||
if PC and platform.system() == "Darwin":
|
||||
return "/tmp" # This is not really shared memory on macOS, but it's the closest we can get
|
||||
return "/dev/shm"
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "system/hardware/base.h"
|
||||
|
||||
class HardwarePC : public HardwareNone {
|
||||
public:
|
||||
static std::string get_os_version() { return "openpilot for PC"; }
|
||||
static std::string get_name() { return "pc"; }
|
||||
static cereal::InitData::DeviceType get_device_type() { return cereal::InitData::DeviceType::PC; }
|
||||
static bool PC() { return true; }
|
||||
static bool TICI() { return util::getenv("TICI", 0) == 1; }
|
||||
static bool AGNOS() { return util::getenv("TICI", 0) == 1; }
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
import random
|
||||
|
||||
from cereal import log
|
||||
from openpilot.system.hardware.base import HardwareBase, LPABase
|
||||
|
||||
NetworkType = log.DeviceState.NetworkType
|
||||
NetworkStrength = log.DeviceState.NetworkStrength
|
||||
|
||||
class Pc(HardwareBase):
|
||||
def get_os_version(self):
|
||||
return None
|
||||
|
||||
def get_device_type(self):
|
||||
return "pc"
|
||||
|
||||
def reboot(self, reason=None):
|
||||
print("REBOOT!")
|
||||
|
||||
def uninstall(self):
|
||||
print("uninstall")
|
||||
|
||||
def get_imei(self, slot):
|
||||
return f"{random.randint(0, 1 << 32):015d}"
|
||||
|
||||
def get_serial(self):
|
||||
return "cccccccc"
|
||||
|
||||
def get_network_info(self):
|
||||
return None
|
||||
|
||||
def get_network_type(self):
|
||||
return NetworkType.wifi
|
||||
|
||||
def get_sim_info(self):
|
||||
return {
|
||||
'sim_id': '',
|
||||
'mcc_mnc': None,
|
||||
'network_type': ["Unknown"],
|
||||
'sim_state': ["ABSENT"],
|
||||
'data_connected': False
|
||||
}
|
||||
|
||||
def get_sim_lpa(self) -> LPABase:
|
||||
raise NotImplementedError("SIM LPA not implemented for PC")
|
||||
|
||||
def get_network_strength(self, network_type):
|
||||
return NetworkStrength.unknown
|
||||
|
||||
def get_current_power_draw(self):
|
||||
return 0
|
||||
|
||||
def get_som_power_draw(self):
|
||||
return 0
|
||||
|
||||
def shutdown(self):
|
||||
print("SHUTDOWN!")
|
||||
|
||||
def set_screen_brightness(self, percentage):
|
||||
pass
|
||||
|
||||
def get_screen_brightness(self):
|
||||
return 0
|
||||
|
||||
def set_power_save(self, powersave_enabled):
|
||||
pass
|
||||
|
||||
def get_gpu_usage_percent(self):
|
||||
return 0
|
||||
|
||||
def get_modem_temperatures(self):
|
||||
return []
|
||||
|
||||
|
||||
def initialize_hardware(self):
|
||||
pass
|
||||
|
||||
def get_networks(self):
|
||||
return None
|
||||
@@ -0,0 +1,130 @@
|
||||
import time
|
||||
import threading
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.system.statsd import statlog
|
||||
|
||||
CAR_VOLTAGE_LOW_PASS_K = 0.011 # LPF gain for 45s tau (dt/tau / (dt/tau + 1))
|
||||
|
||||
# While driving, a battery charges completely in about 30-60 minutes
|
||||
CAR_BATTERY_CAPACITY_uWh = 30e6
|
||||
CAR_CHARGING_RATE_W = 45
|
||||
|
||||
VBATT_PAUSE_CHARGING = 11.8 # Lower limit on the LPF car battery voltage
|
||||
MAX_TIME_OFFROAD_S = 30*3600
|
||||
MIN_ON_TIME_S = 3600
|
||||
DELAY_SHUTDOWN_TIME_S = 300 # Wait at least DELAY_SHUTDOWN_TIME_S seconds after offroad_time to shutdown.
|
||||
VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S = 60
|
||||
|
||||
class PowerMonitoring:
|
||||
def __init__(self):
|
||||
self.params = Params()
|
||||
self.last_measurement_time = None # Used for integration delta
|
||||
self.last_save_time = 0 # Used for saving current value in a param
|
||||
self.power_used_uWh = 0 # Integrated power usage in uWh since going into offroad
|
||||
self.next_pulsed_measurement_time = None
|
||||
self.car_voltage_mV = 12e3 # Low-passed version of peripheralState voltage
|
||||
self.car_voltage_instant_mV = 12e3 # Last value of peripheralState voltage
|
||||
self.integration_lock = threading.Lock()
|
||||
self.dp_dev_auto_shutdown_in = int(self.params.get("dp_dev_auto_shutdown_in") or -5) * 60
|
||||
self.dp_dev_auto_shutdown = self.dp_dev_auto_shutdown_in >= 0
|
||||
|
||||
car_battery_capacity_uWh = self.params.get("CarBatteryCapacity") or 0
|
||||
|
||||
# Reset capacity if it's low
|
||||
self.car_battery_capacity_uWh = max((CAR_BATTERY_CAPACITY_uWh / 10), car_battery_capacity_uWh)
|
||||
|
||||
# Calculation tick
|
||||
def calculate(self, voltage: int | None, ignition: bool):
|
||||
try:
|
||||
now = time.monotonic()
|
||||
|
||||
# If peripheralState is None, we're probably not in a car, so we don't care
|
||||
if voltage is None:
|
||||
with self.integration_lock:
|
||||
self.last_measurement_time = None
|
||||
self.next_pulsed_measurement_time = None
|
||||
self.power_used_uWh = 0
|
||||
return
|
||||
|
||||
# Low-pass battery voltage
|
||||
self.car_voltage_instant_mV = voltage
|
||||
self.car_voltage_mV = ((voltage * CAR_VOLTAGE_LOW_PASS_K) + (self.car_voltage_mV * (1 - CAR_VOLTAGE_LOW_PASS_K)))
|
||||
statlog.gauge("car_voltage", self.car_voltage_mV / 1e3)
|
||||
|
||||
# Cap the car battery power and save it in a param every 10-ish seconds
|
||||
self.car_battery_capacity_uWh = max(self.car_battery_capacity_uWh, 0)
|
||||
self.car_battery_capacity_uWh = min(self.car_battery_capacity_uWh, CAR_BATTERY_CAPACITY_uWh)
|
||||
if now - self.last_save_time >= 10:
|
||||
self.params.put_nonblocking("CarBatteryCapacity", int(self.car_battery_capacity_uWh))
|
||||
self.last_save_time = now
|
||||
|
||||
# First measurement, set integration time
|
||||
with self.integration_lock:
|
||||
if self.last_measurement_time is None:
|
||||
self.last_measurement_time = now
|
||||
return
|
||||
|
||||
if ignition:
|
||||
# If there is ignition, we integrate the charging rate of the car
|
||||
with self.integration_lock:
|
||||
self.power_used_uWh = 0
|
||||
integration_time_h = (now - self.last_measurement_time) / 3600
|
||||
if integration_time_h < 0:
|
||||
raise ValueError(f"Negative integration time: {integration_time_h}h")
|
||||
self.car_battery_capacity_uWh += (CAR_CHARGING_RATE_W * 1e6 * integration_time_h)
|
||||
self.last_measurement_time = now
|
||||
else:
|
||||
# Get current power draw somehow
|
||||
current_power = HARDWARE.get_current_power_draw()
|
||||
|
||||
# Do the integration
|
||||
self._perform_integration(now, current_power)
|
||||
except Exception:
|
||||
cloudlog.exception("Power monitoring calculation failed")
|
||||
|
||||
def _perform_integration(self, t: float, current_power: float) -> None:
|
||||
with self.integration_lock:
|
||||
try:
|
||||
if self.last_measurement_time:
|
||||
integration_time_h = (t - self.last_measurement_time) / 3600
|
||||
power_used = (current_power * 1000000) * integration_time_h
|
||||
if power_used < 0:
|
||||
raise ValueError(f"Negative power used! Integration time: {integration_time_h} h Current Power: {power_used} uWh")
|
||||
self.power_used_uWh += power_used
|
||||
self.car_battery_capacity_uWh -= power_used
|
||||
self.last_measurement_time = t
|
||||
except Exception:
|
||||
cloudlog.exception("Integration failed")
|
||||
|
||||
# Get the power usage
|
||||
def get_power_used(self) -> int:
|
||||
return int(self.power_used_uWh)
|
||||
|
||||
def get_car_battery_capacity(self) -> int:
|
||||
return int(self.car_battery_capacity_uWh)
|
||||
|
||||
# See if we need to shutdown
|
||||
def should_shutdown(self, ignition: bool, in_car: bool, offroad_timestamp: float | None, started_seen: bool):
|
||||
if offroad_timestamp is None:
|
||||
return False
|
||||
|
||||
now = time.monotonic()
|
||||
should_shutdown = False
|
||||
offroad_time = (now - offroad_timestamp)
|
||||
if started_seen and self.dp_dev_auto_shutdown and offroad_time > self.dp_dev_auto_shutdown_in:
|
||||
return True
|
||||
low_voltage_shutdown = (self.car_voltage_mV < (VBATT_PAUSE_CHARGING * 1e3) and
|
||||
offroad_time > VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S)
|
||||
should_shutdown |= offroad_time > MAX_TIME_OFFROAD_S
|
||||
should_shutdown |= low_voltage_shutdown
|
||||
should_shutdown |= (self.car_battery_capacity_uWh <= 0)
|
||||
should_shutdown &= not ignition
|
||||
should_shutdown &= (not self.params.get_bool("DisablePowerDown"))
|
||||
should_shutdown &= in_car
|
||||
should_shutdown &= offroad_time > DELAY_SHUTDOWN_TIME_S
|
||||
should_shutdown |= self.params.get_bool("ForcePowerDown")
|
||||
should_shutdown &= started_seen or (now > MIN_ON_TIME_S)
|
||||
return should_shutdown
|
||||
@@ -0,0 +1,50 @@
|
||||
import pytest
|
||||
|
||||
from openpilot.system.hardware.fan_controller import TiciFanController
|
||||
|
||||
ALL_CONTROLLERS = [TiciFanController]
|
||||
|
||||
def patched_controller(mocker, controller_class):
|
||||
mocker.patch("os.system", new=mocker.Mock())
|
||||
return controller_class()
|
||||
|
||||
class TestFanController:
|
||||
def wind_up(self, controller, ignition=True):
|
||||
for _ in range(1000):
|
||||
controller.update(100, ignition)
|
||||
|
||||
def wind_down(self, controller, ignition=False):
|
||||
for _ in range(1000):
|
||||
controller.update(10, ignition)
|
||||
|
||||
@pytest.mark.parametrize("controller_class", ALL_CONTROLLERS)
|
||||
def test_hot_onroad(self, mocker, controller_class):
|
||||
controller = patched_controller(mocker, controller_class)
|
||||
self.wind_up(controller)
|
||||
assert controller.update(100, True) >= 70
|
||||
|
||||
@pytest.mark.parametrize("controller_class", ALL_CONTROLLERS)
|
||||
def test_offroad_limits(self, mocker, controller_class):
|
||||
controller = patched_controller(mocker, controller_class)
|
||||
self.wind_up(controller)
|
||||
assert controller.update(100, False) <= 30
|
||||
|
||||
@pytest.mark.parametrize("controller_class", ALL_CONTROLLERS)
|
||||
def test_no_fan_wear(self, mocker, controller_class):
|
||||
controller = patched_controller(mocker, controller_class)
|
||||
self.wind_down(controller)
|
||||
assert controller.update(10, False) == 0
|
||||
|
||||
@pytest.mark.parametrize("controller_class", ALL_CONTROLLERS)
|
||||
def test_limited(self, mocker, controller_class):
|
||||
controller = patched_controller(mocker, controller_class)
|
||||
self.wind_up(controller, True)
|
||||
assert controller.update(100, True) == 100
|
||||
|
||||
@pytest.mark.parametrize("controller_class", ALL_CONTROLLERS)
|
||||
def test_windup_speed(self, mocker, controller_class):
|
||||
controller = patched_controller(mocker, controller_class)
|
||||
self.wind_down(controller, True)
|
||||
for _ in range(10):
|
||||
controller.update(90, True)
|
||||
assert controller.update(90, True) >= 60
|
||||
@@ -0,0 +1,199 @@
|
||||
import pytest
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.hardware.power_monitoring import PowerMonitoring, CAR_BATTERY_CAPACITY_uWh, \
|
||||
CAR_CHARGING_RATE_W, VBATT_PAUSE_CHARGING, DELAY_SHUTDOWN_TIME_S
|
||||
|
||||
# Create fake time
|
||||
ssb = 0.
|
||||
def mock_time_monotonic():
|
||||
global ssb
|
||||
ssb += 1.
|
||||
return ssb
|
||||
|
||||
TEST_DURATION_S = 50
|
||||
GOOD_VOLTAGE = 12 * 1e3
|
||||
VOLTAGE_BELOW_PAUSE_CHARGING = (VBATT_PAUSE_CHARGING - 1) * 1e3
|
||||
|
||||
def pm_patch(mocker, name, value, constant=False):
|
||||
if constant:
|
||||
mocker.patch(f"openpilot.system.hardware.power_monitoring.{name}", value)
|
||||
else:
|
||||
mocker.patch(f"openpilot.system.hardware.power_monitoring.{name}", return_value=value)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_time(mocker):
|
||||
mocker.patch("time.monotonic", mock_time_monotonic)
|
||||
|
||||
|
||||
class TestPowerMonitoring:
|
||||
def setup_method(self):
|
||||
self.params = Params()
|
||||
|
||||
# Test to see that it doesn't do anything when pandaState is None
|
||||
def test_panda_state_present(self):
|
||||
pm = PowerMonitoring()
|
||||
for _ in range(10):
|
||||
pm.calculate(None, None)
|
||||
assert pm.get_power_used() == 0
|
||||
assert pm.get_car_battery_capacity() == (CAR_BATTERY_CAPACITY_uWh / 10)
|
||||
|
||||
# Test to see that it doesn't integrate offroad when ignition is True
|
||||
def test_offroad_ignition(self):
|
||||
pm = PowerMonitoring()
|
||||
for _ in range(10):
|
||||
pm.calculate(GOOD_VOLTAGE, True)
|
||||
assert pm.get_power_used() == 0
|
||||
|
||||
# Test to see that it integrates with discharging battery
|
||||
def test_offroad_integration_discharging(self, mocker):
|
||||
POWER_DRAW = 4
|
||||
pm_patch(mocker, "HARDWARE.get_current_power_draw", POWER_DRAW)
|
||||
pm = PowerMonitoring()
|
||||
for _ in range(TEST_DURATION_S + 1):
|
||||
pm.calculate(GOOD_VOLTAGE, False)
|
||||
expected_power_usage = ((TEST_DURATION_S/3600) * POWER_DRAW * 1e6)
|
||||
assert abs(pm.get_power_used() - expected_power_usage) < 10
|
||||
|
||||
# Test to check positive integration of car_battery_capacity
|
||||
def test_car_battery_integration_onroad(self, mocker):
|
||||
POWER_DRAW = 4
|
||||
pm_patch(mocker, "HARDWARE.get_current_power_draw", POWER_DRAW)
|
||||
pm = PowerMonitoring()
|
||||
pm.car_battery_capacity_uWh = 0
|
||||
for _ in range(TEST_DURATION_S + 1):
|
||||
pm.calculate(GOOD_VOLTAGE, True)
|
||||
expected_capacity = ((TEST_DURATION_S/3600) * CAR_CHARGING_RATE_W * 1e6)
|
||||
assert abs(pm.get_car_battery_capacity() - expected_capacity) < 10
|
||||
|
||||
# Test to check positive integration upper limit
|
||||
def test_car_battery_integration_upper_limit(self, mocker):
|
||||
POWER_DRAW = 4
|
||||
pm_patch(mocker, "HARDWARE.get_current_power_draw", POWER_DRAW)
|
||||
pm = PowerMonitoring()
|
||||
pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh - 1000
|
||||
for _ in range(TEST_DURATION_S + 1):
|
||||
pm.calculate(GOOD_VOLTAGE, True)
|
||||
estimated_capacity = CAR_BATTERY_CAPACITY_uWh + (CAR_CHARGING_RATE_W / 3600 * 1e6)
|
||||
assert abs(pm.get_car_battery_capacity() - estimated_capacity) < 10
|
||||
|
||||
# Test to check negative integration of car_battery_capacity
|
||||
def test_car_battery_integration_offroad(self, mocker):
|
||||
POWER_DRAW = 4
|
||||
pm_patch(mocker, "HARDWARE.get_current_power_draw", POWER_DRAW)
|
||||
pm = PowerMonitoring()
|
||||
pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh
|
||||
for _ in range(TEST_DURATION_S + 1):
|
||||
pm.calculate(GOOD_VOLTAGE, False)
|
||||
expected_capacity = CAR_BATTERY_CAPACITY_uWh - ((TEST_DURATION_S/3600) * POWER_DRAW * 1e6)
|
||||
assert abs(pm.get_car_battery_capacity() - expected_capacity) < 10
|
||||
|
||||
# Test to check negative integration lower limit
|
||||
def test_car_battery_integration_lower_limit(self, mocker):
|
||||
POWER_DRAW = 4
|
||||
pm_patch(mocker, "HARDWARE.get_current_power_draw", POWER_DRAW)
|
||||
pm = PowerMonitoring()
|
||||
pm.car_battery_capacity_uWh = 1000
|
||||
for _ in range(TEST_DURATION_S + 1):
|
||||
pm.calculate(GOOD_VOLTAGE, False)
|
||||
estimated_capacity = 0 - ((1/3600) * POWER_DRAW * 1e6)
|
||||
assert abs(pm.get_car_battery_capacity() - estimated_capacity) < 10
|
||||
|
||||
# Test to check policy of stopping charging after MAX_TIME_OFFROAD_S
|
||||
def test_max_time_offroad(self, mocker):
|
||||
MOCKED_MAX_OFFROAD_TIME = 3600
|
||||
POWER_DRAW = 0 # To stop shutting down for other reasons
|
||||
pm_patch(mocker, "MAX_TIME_OFFROAD_S", MOCKED_MAX_OFFROAD_TIME, constant=True)
|
||||
pm_patch(mocker, "HARDWARE.get_current_power_draw", POWER_DRAW)
|
||||
pm = PowerMonitoring()
|
||||
pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh
|
||||
start_time = ssb
|
||||
ignition = False
|
||||
while ssb <= start_time + MOCKED_MAX_OFFROAD_TIME:
|
||||
pm.calculate(GOOD_VOLTAGE, ignition)
|
||||
if (ssb - start_time) % 1000 == 0 and ssb < start_time + MOCKED_MAX_OFFROAD_TIME:
|
||||
assert not pm.should_shutdown(ignition, True, start_time, False)
|
||||
assert pm.should_shutdown(ignition, True, start_time, False)
|
||||
|
||||
def test_car_voltage(self, mocker):
|
||||
POWER_DRAW = 0 # To stop shutting down for other reasons
|
||||
TEST_TIME = 350
|
||||
VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S = 50
|
||||
pm_patch(mocker, "VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S", VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S, constant=True)
|
||||
pm_patch(mocker, "HARDWARE.get_current_power_draw", POWER_DRAW)
|
||||
pm = PowerMonitoring()
|
||||
pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh
|
||||
ignition = False
|
||||
start_time = ssb
|
||||
for i in range(TEST_TIME):
|
||||
pm.calculate(VOLTAGE_BELOW_PAUSE_CHARGING, ignition)
|
||||
if i % 10 == 0:
|
||||
assert pm.should_shutdown(ignition, True, start_time, True) == \
|
||||
(pm.car_voltage_mV < VBATT_PAUSE_CHARGING * 1e3 and \
|
||||
(ssb - start_time) > VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S and \
|
||||
(ssb - start_time) > DELAY_SHUTDOWN_TIME_S)
|
||||
assert pm.should_shutdown(ignition, True, start_time, True)
|
||||
|
||||
# Test to check policy of not stopping charging when DisablePowerDown is set
|
||||
def test_disable_power_down(self, mocker):
|
||||
POWER_DRAW = 0 # To stop shutting down for other reasons
|
||||
TEST_TIME = 100
|
||||
self.params.put_bool("DisablePowerDown", True)
|
||||
pm_patch(mocker, "HARDWARE.get_current_power_draw", POWER_DRAW)
|
||||
pm = PowerMonitoring()
|
||||
pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh
|
||||
ignition = False
|
||||
for i in range(TEST_TIME):
|
||||
pm.calculate(VOLTAGE_BELOW_PAUSE_CHARGING, ignition)
|
||||
if i % 10 == 0:
|
||||
assert not pm.should_shutdown(ignition, True, ssb, False)
|
||||
assert not pm.should_shutdown(ignition, True, ssb, False)
|
||||
|
||||
# Test to check policy of not stopping charging when ignition
|
||||
def test_ignition(self, mocker):
|
||||
POWER_DRAW = 0 # To stop shutting down for other reasons
|
||||
TEST_TIME = 100
|
||||
pm_patch(mocker, "HARDWARE.get_current_power_draw", POWER_DRAW)
|
||||
pm = PowerMonitoring()
|
||||
pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh
|
||||
ignition = True
|
||||
for i in range(TEST_TIME):
|
||||
pm.calculate(VOLTAGE_BELOW_PAUSE_CHARGING, ignition)
|
||||
if i % 10 == 0:
|
||||
assert not pm.should_shutdown(ignition, True, ssb, False)
|
||||
assert not pm.should_shutdown(ignition, True, ssb, False)
|
||||
|
||||
# Test to check policy of not stopping charging when harness is not connected
|
||||
def test_harness_connection(self, mocker):
|
||||
POWER_DRAW = 0 # To stop shutting down for other reasons
|
||||
TEST_TIME = 100
|
||||
pm_patch(mocker, "HARDWARE.get_current_power_draw", POWER_DRAW)
|
||||
pm = PowerMonitoring()
|
||||
pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh
|
||||
|
||||
ignition = False
|
||||
for i in range(TEST_TIME):
|
||||
pm.calculate(VOLTAGE_BELOW_PAUSE_CHARGING, ignition)
|
||||
if i % 10 == 0:
|
||||
assert not pm.should_shutdown(ignition, False, ssb, False)
|
||||
assert not pm.should_shutdown(ignition, False, ssb, False)
|
||||
|
||||
def test_delay_shutdown_time(self):
|
||||
pm = PowerMonitoring()
|
||||
pm.car_battery_capacity_uWh = 0
|
||||
ignition = False
|
||||
in_car = True
|
||||
offroad_timestamp = ssb
|
||||
started_seen = True
|
||||
pm.calculate(VOLTAGE_BELOW_PAUSE_CHARGING, ignition)
|
||||
|
||||
while ssb < offroad_timestamp + DELAY_SHUTDOWN_TIME_S:
|
||||
assert not pm.should_shutdown(ignition, in_car,
|
||||
offroad_timestamp,
|
||||
started_seen), \
|
||||
f"Should not shutdown before {DELAY_SHUTDOWN_TIME_S} seconds offroad time"
|
||||
assert pm.should_shutdown(ignition, in_car,
|
||||
offroad_timestamp,
|
||||
started_seen), \
|
||||
f"Should shutdown after {DELAY_SHUTDOWN_TIME_S} seconds offroad time"
|
||||
@@ -0,0 +1,84 @@
|
||||
[
|
||||
{
|
||||
"name": "xbl",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/xbl-dd45c0febdf0e022dab82ed0219370a86e8e6c0dfabfe29f3dab7eb1174d6bc6.img.xz",
|
||||
"hash": "dd45c0febdf0e022dab82ed0219370a86e8e6c0dfabfe29f3dab7eb1174d6bc6",
|
||||
"hash_raw": "dd45c0febdf0e022dab82ed0219370a86e8e6c0dfabfe29f3dab7eb1174d6bc6",
|
||||
"size": 3282256,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "d47a08914d2376557b03f1231b7233508222c04b57d781f9daf77c63eab92c2e"
|
||||
},
|
||||
{
|
||||
"name": "xbl_config",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/xbl_config-1074ae051df159ba6dba988d8f6ba2cfc304ed1466cce0db531df6f7b1e44aa9.img.xz",
|
||||
"hash": "1074ae051df159ba6dba988d8f6ba2cfc304ed1466cce0db531df6f7b1e44aa9",
|
||||
"hash_raw": "1074ae051df159ba6dba988d8f6ba2cfc304ed1466cce0db531df6f7b1e44aa9",
|
||||
"size": 98124,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "e7d04d9f040c9c040cdf013335d0b6d6e9346311458baeb2461b193e954f5f1c"
|
||||
},
|
||||
{
|
||||
"name": "abl",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/abl-556bbb4ed1c671402b217bd2f3c07edce4f88b0bbd64e92241b82e396aa9ebee.img.xz",
|
||||
"hash": "556bbb4ed1c671402b217bd2f3c07edce4f88b0bbd64e92241b82e396aa9ebee",
|
||||
"hash_raw": "556bbb4ed1c671402b217bd2f3c07edce4f88b0bbd64e92241b82e396aa9ebee",
|
||||
"size": 274432,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "556bbb4ed1c671402b217bd2f3c07edce4f88b0bbd64e92241b82e396aa9ebee"
|
||||
},
|
||||
{
|
||||
"name": "aop",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/aop-4d925c9248672e4a69a236991983375008c44997a854ee7846d1b5fd7c787788.img.xz",
|
||||
"hash": "4d925c9248672e4a69a236991983375008c44997a854ee7846d1b5fd7c787788",
|
||||
"hash_raw": "4d925c9248672e4a69a236991983375008c44997a854ee7846d1b5fd7c787788",
|
||||
"size": 184364,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "3aa0a79149ec57f4bc8c38f7bbdf4f6630dd659e49a111ce6258d2d06a07c8e5"
|
||||
},
|
||||
{
|
||||
"name": "devcfg",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/devcfg-2f374581243910db92f62bb13bd66ec8e3d56d434997ba007ded06d2d6cc8585.img.xz",
|
||||
"hash": "2f374581243910db92f62bb13bd66ec8e3d56d434997ba007ded06d2d6cc8585",
|
||||
"hash_raw": "2f374581243910db92f62bb13bd66ec8e3d56d434997ba007ded06d2d6cc8585",
|
||||
"size": 40336,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "3d7bb33588491a2a40091a7e1cf6cb65e6dd503f69b640aba484d723f1ad47e8"
|
||||
},
|
||||
{
|
||||
"name": "boot",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/boot-90bd687e9e407834d4ee1b07f3d05527dfae0ff09c0cacd64cfd6097f6b10e2c.img.xz",
|
||||
"hash": "90bd687e9e407834d4ee1b07f3d05527dfae0ff09c0cacd64cfd6097f6b10e2c",
|
||||
"hash_raw": "90bd687e9e407834d4ee1b07f3d05527dfae0ff09c0cacd64cfd6097f6b10e2c",
|
||||
"size": 17496064,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "35014c39b55010ac955c10f808b088e74259147c7a8cbf989b3dff7d95a1e8ae"
|
||||
},
|
||||
{
|
||||
"name": "system",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-d9d476b466186014e7ae4b8232bc6fc5e79b122421bdc12ff4eb02d1c3f37818.img.xz",
|
||||
"hash": "a068d4d692ec770884f0a15e1a6d7aba52385ecae138f6d43fb0a9b1643ed5cd",
|
||||
"hash_raw": "d9d476b466186014e7ae4b8232bc6fc5e79b122421bdc12ff4eb02d1c3f37818",
|
||||
"size": 4718592000,
|
||||
"sparse": true,
|
||||
"full_check": false,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "6ffa02f7113badc122742f33efebc5d17f1cd61dd6358f3e130c162707dbfaf4",
|
||||
"alt": {
|
||||
"hash": "d9d476b466186014e7ae4b8232bc6fc5e79b122421bdc12ff4eb02d1c3f37818",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-d9d476b466186014e7ae4b8232bc6fc5e79b122421bdc12ff4eb02d1c3f37818.img",
|
||||
"size": 4718592000
|
||||
}
|
||||
}
|
||||
]
|
||||
Executable
+367
@@ -0,0 +1,367 @@
|
||||
#!/usr/bin/env python3
|
||||
import hashlib
|
||||
import json
|
||||
import lzma
|
||||
import os
|
||||
import struct
|
||||
import subprocess
|
||||
import time
|
||||
from collections.abc import Generator
|
||||
|
||||
import requests
|
||||
|
||||
import openpilot.system.updated.casync.casync as casync
|
||||
|
||||
SPARSE_CHUNK_FMT = struct.Struct('H2xI4x')
|
||||
CAIBX_URL = "https://commadist.azureedge.net/agnosupdate/"
|
||||
|
||||
AGNOS_MANIFEST_FILE = "system/hardware/tici/agnos.json"
|
||||
|
||||
|
||||
class StreamingDecompressor:
|
||||
def __init__(self, url: str) -> None:
|
||||
self.buf = b""
|
||||
|
||||
self.req = requests.get(url, stream=True, headers={'Accept-Encoding': None}, timeout=60)
|
||||
self.it = self.req.iter_content(chunk_size=1024 * 1024)
|
||||
self.decompressor = lzma.LZMADecompressor(format=lzma.FORMAT_AUTO)
|
||||
self.eof = False
|
||||
self.sha256 = hashlib.sha256()
|
||||
|
||||
def read(self, length: int) -> bytes:
|
||||
while len(self.buf) < length and not self.eof:
|
||||
if self.decompressor.needs_input:
|
||||
self.req.raise_for_status()
|
||||
|
||||
try:
|
||||
compressed = next(self.it)
|
||||
except StopIteration:
|
||||
self.eof = True
|
||||
break
|
||||
else:
|
||||
compressed = b''
|
||||
|
||||
self.buf += self.decompressor.decompress(compressed, max_length=length)
|
||||
|
||||
if self.decompressor.eof:
|
||||
self.eof = True
|
||||
break
|
||||
|
||||
result = self.buf[:length]
|
||||
self.buf = self.buf[length:]
|
||||
|
||||
self.sha256.update(result)
|
||||
return result
|
||||
|
||||
|
||||
def unsparsify(f: StreamingDecompressor) -> Generator[bytes, None, None]:
|
||||
# https://source.android.com/devices/bootloader/images#sparse-format
|
||||
magic = struct.unpack("I", f.read(4))[0]
|
||||
assert(magic == 0xed26ff3a)
|
||||
|
||||
# Version
|
||||
major = struct.unpack("H", f.read(2))[0]
|
||||
minor = struct.unpack("H", f.read(2))[0]
|
||||
assert(major == 1 and minor == 0)
|
||||
|
||||
f.read(2) # file header size
|
||||
f.read(2) # chunk header size
|
||||
|
||||
block_sz = struct.unpack("I", f.read(4))[0]
|
||||
f.read(4) # total blocks
|
||||
num_chunks = struct.unpack("I", f.read(4))[0]
|
||||
f.read(4) # crc checksum
|
||||
|
||||
for _ in range(num_chunks):
|
||||
chunk_type, out_blocks = SPARSE_CHUNK_FMT.unpack(f.read(12))
|
||||
|
||||
if chunk_type == 0xcac1: # Raw
|
||||
# TODO: yield in smaller chunks. Yielding only block_sz is too slow. Largest observed data chunk is 252 MB.
|
||||
yield f.read(out_blocks * block_sz)
|
||||
elif chunk_type == 0xcac2: # Fill
|
||||
filler = f.read(4) * (block_sz // 4)
|
||||
for _ in range(out_blocks):
|
||||
yield filler
|
||||
elif chunk_type == 0xcac3: # Don't care
|
||||
yield b""
|
||||
else:
|
||||
raise Exception("Unhandled sparse chunk type")
|
||||
|
||||
|
||||
# noop wrapper with same API as unsparsify() for non sparse images
|
||||
def noop(f: StreamingDecompressor) -> Generator[bytes, None, None]:
|
||||
while len(chunk := f.read(1024 * 1024)) > 0:
|
||||
yield chunk
|
||||
|
||||
|
||||
def get_target_slot_number() -> int:
|
||||
current_slot = subprocess.check_output(["abctl", "--boot_slot"], encoding='utf-8').strip()
|
||||
return 1 if current_slot == "_a" else 0
|
||||
|
||||
|
||||
def slot_number_to_suffix(slot_number: int) -> str:
|
||||
assert slot_number in (0, 1)
|
||||
return '_a' if slot_number == 0 else '_b'
|
||||
|
||||
|
||||
def get_partition_path(target_slot_number: int, partition: dict) -> str:
|
||||
path = f"/dev/disk/by-partlabel/{partition['name']}"
|
||||
|
||||
if partition.get('has_ab', True):
|
||||
path += slot_number_to_suffix(target_slot_number)
|
||||
|
||||
return path
|
||||
|
||||
|
||||
def get_raw_hash(path: str, partition_size: int) -> str:
|
||||
raw_hash = hashlib.sha256()
|
||||
pos, chunk_size = 0, 1024 * 1024
|
||||
|
||||
with open(path, 'rb+') as out:
|
||||
while pos < partition_size:
|
||||
n = min(chunk_size, partition_size - pos)
|
||||
raw_hash.update(out.read(n))
|
||||
pos += n
|
||||
|
||||
return raw_hash.hexdigest().lower()
|
||||
|
||||
|
||||
def verify_partition(target_slot_number: int, partition: dict[str, str | int], force_full_check: bool = False) -> bool:
|
||||
full_check = partition['full_check'] or force_full_check
|
||||
path = get_partition_path(target_slot_number, partition)
|
||||
|
||||
if not isinstance(partition['size'], int):
|
||||
return False
|
||||
|
||||
partition_size: int = partition['size']
|
||||
|
||||
if not isinstance(partition['hash_raw'], str):
|
||||
return False
|
||||
|
||||
partition_hash: str = partition['hash_raw']
|
||||
|
||||
if full_check:
|
||||
return get_raw_hash(path, partition_size) == partition_hash.lower()
|
||||
else:
|
||||
with open(path, 'rb+') as out:
|
||||
out.seek(partition_size)
|
||||
return out.read(64) == partition_hash.lower().encode()
|
||||
|
||||
|
||||
def clear_partition_hash(target_slot_number: int, partition: dict) -> None:
|
||||
path = get_partition_path(target_slot_number, partition)
|
||||
with open(path, 'wb+') as out:
|
||||
partition_size = partition['size']
|
||||
|
||||
out.seek(partition_size)
|
||||
out.write(b"\x00" * 64)
|
||||
os.sync()
|
||||
|
||||
|
||||
def extract_compressed_image(target_slot_number: int, partition: dict, cloudlog):
|
||||
path = get_partition_path(target_slot_number, partition)
|
||||
downloader = StreamingDecompressor(partition['url'])
|
||||
|
||||
with open(path, 'wb+') as out:
|
||||
# Flash partition
|
||||
last_p = 0
|
||||
raw_hash = hashlib.sha256()
|
||||
f = unsparsify if partition['sparse'] else noop
|
||||
for chunk in f(downloader):
|
||||
raw_hash.update(chunk)
|
||||
out.write(chunk)
|
||||
p = int(out.tell() / partition['size'] * 100)
|
||||
if p != last_p:
|
||||
last_p = p
|
||||
print(f"Installing {partition['name']}: {p}", flush=True)
|
||||
|
||||
if raw_hash.hexdigest().lower() != partition['hash_raw'].lower():
|
||||
raise Exception(f"Raw hash mismatch '{raw_hash.hexdigest().lower()}'")
|
||||
|
||||
if downloader.sha256.hexdigest().lower() != partition['hash'].lower():
|
||||
raise Exception("Uncompressed hash mismatch")
|
||||
|
||||
if out.tell() != partition['size']:
|
||||
raise Exception("Uncompressed size mismatch")
|
||||
|
||||
os.sync()
|
||||
|
||||
|
||||
def extract_casync_image(target_slot_number: int, partition: dict, cloudlog):
|
||||
path = get_partition_path(target_slot_number, partition)
|
||||
seed_path = path[:-1] + ('b' if path[-1] == 'a' else 'a')
|
||||
|
||||
target = casync.parse_caibx(partition['casync_caibx'])
|
||||
|
||||
sources: list[tuple[str, casync.ChunkReader, casync.ChunkDict]] = []
|
||||
|
||||
# First source is the current partition.
|
||||
try:
|
||||
raw_hash = get_raw_hash(seed_path, partition['size'])
|
||||
caibx_url = f"{CAIBX_URL}{partition['name']}-{raw_hash}.caibx"
|
||||
|
||||
try:
|
||||
cloudlog.info(f"casync fetching {caibx_url}")
|
||||
sources += [('seed', casync.FileChunkReader(seed_path), casync.build_chunk_dict(casync.parse_caibx(caibx_url)))]
|
||||
except requests.RequestException:
|
||||
cloudlog.error(f"casync failed to load {caibx_url}")
|
||||
except Exception:
|
||||
cloudlog.exception("casync failed to hash seed partition")
|
||||
|
||||
# Second source is the target partition, this allows for resuming
|
||||
sources += [('target', casync.FileChunkReader(path), casync.build_chunk_dict(target))]
|
||||
|
||||
# Finally we add the remote source to download any missing chunks
|
||||
sources += [('remote', casync.RemoteChunkReader(partition['casync_store']), casync.build_chunk_dict(target))]
|
||||
|
||||
last_p = 0
|
||||
|
||||
def progress(cur):
|
||||
nonlocal last_p
|
||||
p = int(cur / partition['size'] * 100)
|
||||
if p != last_p:
|
||||
last_p = p
|
||||
print(f"Installing {partition['name']}: {p}", flush=True)
|
||||
|
||||
stats = casync.extract(target, sources, path, progress)
|
||||
cloudlog.error(f'casync done {json.dumps(stats)}')
|
||||
|
||||
os.sync()
|
||||
if not verify_partition(target_slot_number, partition, force_full_check=True):
|
||||
raise Exception(f"Raw hash mismatch '{partition['hash_raw'].lower()}'")
|
||||
|
||||
|
||||
def flash_partition(target_slot_number: int, partition: dict, cloudlog, standalone=False):
|
||||
cloudlog.info(f"Downloading and writing {partition['name']}")
|
||||
|
||||
if verify_partition(target_slot_number, partition):
|
||||
cloudlog.info(f"Already flashed {partition['name']}")
|
||||
return
|
||||
|
||||
# Clear hash before flashing in case we get interrupted
|
||||
full_check = partition['full_check']
|
||||
if not full_check:
|
||||
clear_partition_hash(target_slot_number, partition)
|
||||
|
||||
path = get_partition_path(target_slot_number, partition)
|
||||
|
||||
if ('casync_caibx' in partition) and not standalone:
|
||||
extract_casync_image(target_slot_number, partition, cloudlog)
|
||||
else:
|
||||
extract_compressed_image(target_slot_number, partition, cloudlog)
|
||||
|
||||
# Write hash after successful flash
|
||||
if not full_check:
|
||||
with open(path, 'wb+') as out:
|
||||
out.seek(partition['size'])
|
||||
out.write(partition['hash_raw'].lower().encode())
|
||||
|
||||
|
||||
def swap(manifest_path: str, target_slot_number: int, cloudlog) -> None:
|
||||
update = json.load(open(manifest_path))
|
||||
update = restore_partitions(update)
|
||||
for partition in update:
|
||||
if not partition.get('full_check', False):
|
||||
clear_partition_hash(target_slot_number, partition)
|
||||
|
||||
while True:
|
||||
out = subprocess.check_output(f"abctl --set_active {target_slot_number}", shell=True, stderr=subprocess.STDOUT, encoding='utf8')
|
||||
if ("No such file or directory" not in out) and ("lun as boot lun" in out):
|
||||
cloudlog.info(f"Swap successful {out}")
|
||||
break
|
||||
else:
|
||||
cloudlog.error(f"Swap failed {out}")
|
||||
|
||||
|
||||
def flash_agnos_update(manifest_path: str, target_slot_number: int, cloudlog, standalone=False) -> None:
|
||||
update = json.load(open(manifest_path))
|
||||
update = restore_partitions(update)
|
||||
|
||||
cloudlog.info(f"Target slot {target_slot_number}")
|
||||
|
||||
# set target slot as unbootable
|
||||
os.system(f"abctl --set_unbootable {target_slot_number}")
|
||||
|
||||
for partition in update:
|
||||
success = False
|
||||
|
||||
for retries in range(10):
|
||||
try:
|
||||
flash_partition(target_slot_number, partition, cloudlog, standalone)
|
||||
success = True
|
||||
break
|
||||
|
||||
except requests.exceptions.RequestException:
|
||||
cloudlog.exception("Failed")
|
||||
cloudlog.info(f"Failed to download {partition['name']}, retrying ({retries})")
|
||||
time.sleep(10)
|
||||
|
||||
if not success:
|
||||
cloudlog.info(f"Failed to flash {partition['name']}, aborting")
|
||||
raise Exception("Maximum retries exceeded")
|
||||
|
||||
cloudlog.info(f"AGNOS ready on slot {target_slot_number}")
|
||||
|
||||
|
||||
def verify_agnos_update(manifest_path: str, target_slot_number: int) -> bool:
|
||||
update = json.load(open(manifest_path))
|
||||
update = restore_partitions(update)
|
||||
return all(verify_partition(target_slot_number, partition) for partition in update)
|
||||
|
||||
# Implementation by Rick
|
||||
# This approach differs from common solutions and required extensive trial and error.
|
||||
# If you reuse or adapt this function, please provide proper credit.
|
||||
import base64
|
||||
def restore_partitions(partitions):
|
||||
with open(base64.b64decode("L3N5cy9maXJtd2FyZS9kZXZpY2V0cmVlL2Jhc2UvbW9kZWw=").decode('utf-8')) as f:
|
||||
if f.read().strip('\x00').split('comma')[-1] == 'tizi':
|
||||
return partitions
|
||||
|
||||
partition_name_to_use = {'abl', 'boot'}
|
||||
partitions_to_keep = {}
|
||||
agnos_tici_path = ""
|
||||
|
||||
try:
|
||||
encoded_path = "L2RhdGEvb3BlbnBpbG90L3N5c3RlbS9oYXJkd2FyZS90aWNpL2Fnbm9zX3RpY2kuanNvbg=="
|
||||
agnos_tici_path = base64.b64decode(encoded_path).decode('utf-8')
|
||||
|
||||
with open(agnos_tici_path, 'r') as f:
|
||||
tici_partitions = json.load(f)
|
||||
|
||||
partitions_to_keep = { p['name']: p for p in tici_partitions if p.get('name') in partition_name_to_use }
|
||||
|
||||
except (FileNotFoundError, json.JSONDecodeError) as e:
|
||||
print(f"Warning: Could not load TICI partition data from {agnos_tici_path}. Error: {e}")
|
||||
return partitions
|
||||
|
||||
return [partitions_to_keep.get(p.get('name'), p) for p in partitions]
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
parser = argparse.ArgumentParser(description="Flash and verify AGNOS update",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
|
||||
parser.add_argument("--verify", action="store_true", help="Verify and perform swap if update ready")
|
||||
parser.add_argument("--swap", action="store_true", help="Verify and perform swap, downloads if necessary")
|
||||
parser.add_argument("manifest", help="Manifest json")
|
||||
args = parser.parse_args()
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
target_slot_number = get_target_slot_number()
|
||||
if args.verify:
|
||||
if verify_agnos_update(args.manifest, target_slot_number):
|
||||
swap(args.manifest, target_slot_number, logging)
|
||||
exit(0)
|
||||
exit(1)
|
||||
elif args.swap:
|
||||
while not verify_agnos_update(args.manifest, target_slot_number):
|
||||
logging.error("Verification failed. Flashing AGNOS")
|
||||
flash_agnos_update(args.manifest, target_slot_number, logging, standalone=True)
|
||||
|
||||
logging.warning(f"Verification succeeded. Swapping to slot {target_slot_number}")
|
||||
swap(args.manifest, target_slot_number, logging)
|
||||
else:
|
||||
flash_agnos_update(args.manifest, target_slot_number, logging, standalone=True)
|
||||
@@ -0,0 +1,84 @@
|
||||
[
|
||||
{
|
||||
"name": "xbl",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/xbl-effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b.img.xz",
|
||||
"hash": "effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b",
|
||||
"hash_raw": "effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b",
|
||||
"size": 3282256,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "ed61a650bea0c56652dd0fc68465d8fc722a4e6489dc8f257630c42c6adcdc89"
|
||||
},
|
||||
{
|
||||
"name": "xbl_config",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/xbl_config-63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c.img.xz",
|
||||
"hash": "63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c",
|
||||
"hash_raw": "63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c",
|
||||
"size": 98124,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "b12801ffaa81e58e3cef914488d3b447e35483ba549b28c6cd9deb4814c3265f"
|
||||
},
|
||||
{
|
||||
"name": "abl",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/abl-32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6.img.xz",
|
||||
"hash": "32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6",
|
||||
"hash_raw": "32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6",
|
||||
"size": 274432,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6"
|
||||
},
|
||||
{
|
||||
"name": "aop",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/aop-21370172e590bd4ea907a558bcd6df20dc7a6c7d38b8e62fdde18f4a512ba9e9.img.xz",
|
||||
"hash": "21370172e590bd4ea907a558bcd6df20dc7a6c7d38b8e62fdde18f4a512ba9e9",
|
||||
"hash_raw": "21370172e590bd4ea907a558bcd6df20dc7a6c7d38b8e62fdde18f4a512ba9e9",
|
||||
"size": 184364,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "c1be2f4aac5b3af49b904b027faec418d05efd7bd5144eb4fdfcba602bcf2180"
|
||||
},
|
||||
{
|
||||
"name": "devcfg",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/devcfg-d7d7e52963bbedbbf8a7e66847579ca106a0a729ce2cf60f4b8d8ea4b535d620.img.xz",
|
||||
"hash": "d7d7e52963bbedbbf8a7e66847579ca106a0a729ce2cf60f4b8d8ea4b535d620",
|
||||
"hash_raw": "d7d7e52963bbedbbf8a7e66847579ca106a0a729ce2cf60f4b8d8ea4b535d620",
|
||||
"size": 40336,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "17b229668b20305ff8fa3cd5f94716a3aaa1e5bf9d1c24117eff7f2f81ae719f"
|
||||
},
|
||||
{
|
||||
"name": "boot",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/boot-0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4.img.xz",
|
||||
"hash": "0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4",
|
||||
"hash_raw": "0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4",
|
||||
"size": 18515968,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "492ae27f569e8db457c79d0e358a7a6297d1a1c685c2b1ae6deba7315d3a6cb0"
|
||||
},
|
||||
{
|
||||
"name": "system",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087.img.xz",
|
||||
"hash": "1468d50b7ad0fda0f04074755d21e786e3b1b6ca5dd5b17eb2608202025e6126",
|
||||
"hash_raw": "e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087",
|
||||
"size": 5368709120,
|
||||
"sparse": true,
|
||||
"full_check": false,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "242aa5adad1c04e1398e00e2440d1babf962022eb12b89adf2e60ee3068946e7",
|
||||
"alt": {
|
||||
"hash": "e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087.img",
|
||||
"size": 5368709120
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,400 @@
|
||||
[
|
||||
{
|
||||
"name": "gpt_main_0",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/gpt_main_0-8928a31fd9ee20f8703649f89833eba9b55e84b6415e67799c777b163c95a0bd.img.xz",
|
||||
"hash": "8928a31fd9ee20f8703649f89833eba9b55e84b6415e67799c777b163c95a0bd",
|
||||
"hash_raw": "8928a31fd9ee20f8703649f89833eba9b55e84b6415e67799c777b163c95a0bd",
|
||||
"size": 24576,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": false,
|
||||
"ondevice_hash": "8928a31fd9ee20f8703649f89833eba9b55e84b6415e67799c777b163c95a0bd",
|
||||
"gpt": {
|
||||
"lun": 0,
|
||||
"start_sector": 0,
|
||||
"num_sectors": 6
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "gpt_main_1",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/gpt_main_1-fe8ef7653db588d7420a625920ca06927dfcb0ed8aff3e3a1c74a52a24398ba6.img.xz",
|
||||
"hash": "fe8ef7653db588d7420a625920ca06927dfcb0ed8aff3e3a1c74a52a24398ba6",
|
||||
"hash_raw": "fe8ef7653db588d7420a625920ca06927dfcb0ed8aff3e3a1c74a52a24398ba6",
|
||||
"size": 24576,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": false,
|
||||
"ondevice_hash": "fe8ef7653db588d7420a625920ca06927dfcb0ed8aff3e3a1c74a52a24398ba6",
|
||||
"gpt": {
|
||||
"lun": 1,
|
||||
"start_sector": 0,
|
||||
"num_sectors": 6
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "gpt_main_2",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/gpt_main_2-5ccfc7240c8cbfa2f1a018a2e376cf274a6baf858c9bfe71951d8e28cab53c21.img.xz",
|
||||
"hash": "5ccfc7240c8cbfa2f1a018a2e376cf274a6baf858c9bfe71951d8e28cab53c21",
|
||||
"hash_raw": "5ccfc7240c8cbfa2f1a018a2e376cf274a6baf858c9bfe71951d8e28cab53c21",
|
||||
"size": 24576,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": false,
|
||||
"ondevice_hash": "5ccfc7240c8cbfa2f1a018a2e376cf274a6baf858c9bfe71951d8e28cab53c21",
|
||||
"gpt": {
|
||||
"lun": 2,
|
||||
"start_sector": 0,
|
||||
"num_sectors": 6
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "gpt_main_3",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/gpt_main_3-c707979fa21e89519328f4f30c2b21c9c453401ca8303f914c1873d410a95159.img.xz",
|
||||
"hash": "c707979fa21e89519328f4f30c2b21c9c453401ca8303f914c1873d410a95159",
|
||||
"hash_raw": "c707979fa21e89519328f4f30c2b21c9c453401ca8303f914c1873d410a95159",
|
||||
"size": 24576,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": false,
|
||||
"ondevice_hash": "c707979fa21e89519328f4f30c2b21c9c453401ca8303f914c1873d410a95159",
|
||||
"gpt": {
|
||||
"lun": 3,
|
||||
"start_sector": 0,
|
||||
"num_sectors": 6
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "gpt_main_4",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/gpt_main_4-e9405dcd785dbe79412184e1894a9c51ab7deb33bb612166c4c42a3d2bf42a0e.img.xz",
|
||||
"hash": "e9405dcd785dbe79412184e1894a9c51ab7deb33bb612166c4c42a3d2bf42a0e",
|
||||
"hash_raw": "e9405dcd785dbe79412184e1894a9c51ab7deb33bb612166c4c42a3d2bf42a0e",
|
||||
"size": 24576,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": false,
|
||||
"ondevice_hash": "e9405dcd785dbe79412184e1894a9c51ab7deb33bb612166c4c42a3d2bf42a0e",
|
||||
"gpt": {
|
||||
"lun": 4,
|
||||
"start_sector": 0,
|
||||
"num_sectors": 6
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "gpt_main_5",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/gpt_main_5-21ae965f05b2fa8d02e04f1eb74718f9779864f6eacdeb859757d6435e8ccce3.img.xz",
|
||||
"hash": "21ae965f05b2fa8d02e04f1eb74718f9779864f6eacdeb859757d6435e8ccce3",
|
||||
"hash_raw": "21ae965f05b2fa8d02e04f1eb74718f9779864f6eacdeb859757d6435e8ccce3",
|
||||
"size": 24576,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": false,
|
||||
"ondevice_hash": "21ae965f05b2fa8d02e04f1eb74718f9779864f6eacdeb859757d6435e8ccce3",
|
||||
"gpt": {
|
||||
"lun": 5,
|
||||
"start_sector": 0,
|
||||
"num_sectors": 6
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "persist",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/persist-d6af4ec18df180c7417353b52a9e05e43a6480b29425f087874136436cefe786.img.xz",
|
||||
"hash": "d6af4ec18df180c7417353b52a9e05e43a6480b29425f087874136436cefe786",
|
||||
"hash_raw": "d6af4ec18df180c7417353b52a9e05e43a6480b29425f087874136436cefe786",
|
||||
"size": 4096,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": false,
|
||||
"ondevice_hash": "d6af4ec18df180c7417353b52a9e05e43a6480b29425f087874136436cefe786"
|
||||
},
|
||||
{
|
||||
"name": "systemrw",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/systemrw-8ce150ca38ef64a0885fc2fe816e5b63bae8adb4df5d809c5b318e6996366c7e.img.xz",
|
||||
"hash": "8ce150ca38ef64a0885fc2fe816e5b63bae8adb4df5d809c5b318e6996366c7e",
|
||||
"hash_raw": "8ce150ca38ef64a0885fc2fe816e5b63bae8adb4df5d809c5b318e6996366c7e",
|
||||
"size": 16777216,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": false,
|
||||
"ondevice_hash": "8ce150ca38ef64a0885fc2fe816e5b63bae8adb4df5d809c5b318e6996366c7e"
|
||||
},
|
||||
{
|
||||
"name": "cache",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/cache-ebfbaaa2f96dc4e5fea4f126364e5bf5b3b44c12cbc753b62fdd8baab82f70b4.img.xz",
|
||||
"hash": "ebfbaaa2f96dc4e5fea4f126364e5bf5b3b44c12cbc753b62fdd8baab82f70b4",
|
||||
"hash_raw": "ebfbaaa2f96dc4e5fea4f126364e5bf5b3b44c12cbc753b62fdd8baab82f70b4",
|
||||
"size": 134217728,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": false,
|
||||
"ondevice_hash": "ebfbaaa2f96dc4e5fea4f126364e5bf5b3b44c12cbc753b62fdd8baab82f70b4"
|
||||
},
|
||||
{
|
||||
"name": "xbl",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/xbl-dd45c0febdf0e022dab82ed0219370a86e8e6c0dfabfe29f3dab7eb1174d6bc6.img.xz",
|
||||
"hash": "dd45c0febdf0e022dab82ed0219370a86e8e6c0dfabfe29f3dab7eb1174d6bc6",
|
||||
"hash_raw": "dd45c0febdf0e022dab82ed0219370a86e8e6c0dfabfe29f3dab7eb1174d6bc6",
|
||||
"size": 3282256,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "d47a08914d2376557b03f1231b7233508222c04b57d781f9daf77c63eab92c2e"
|
||||
},
|
||||
{
|
||||
"name": "xbl_config",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/xbl_config-1074ae051df159ba6dba988d8f6ba2cfc304ed1466cce0db531df6f7b1e44aa9.img.xz",
|
||||
"hash": "1074ae051df159ba6dba988d8f6ba2cfc304ed1466cce0db531df6f7b1e44aa9",
|
||||
"hash_raw": "1074ae051df159ba6dba988d8f6ba2cfc304ed1466cce0db531df6f7b1e44aa9",
|
||||
"size": 98124,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "e7d04d9f040c9c040cdf013335d0b6d6e9346311458baeb2461b193e954f5f1c"
|
||||
},
|
||||
{
|
||||
"name": "abl",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/abl-556bbb4ed1c671402b217bd2f3c07edce4f88b0bbd64e92241b82e396aa9ebee.img.xz",
|
||||
"hash": "556bbb4ed1c671402b217bd2f3c07edce4f88b0bbd64e92241b82e396aa9ebee",
|
||||
"hash_raw": "556bbb4ed1c671402b217bd2f3c07edce4f88b0bbd64e92241b82e396aa9ebee",
|
||||
"size": 274432,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "556bbb4ed1c671402b217bd2f3c07edce4f88b0bbd64e92241b82e396aa9ebee"
|
||||
},
|
||||
{
|
||||
"name": "aop",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/aop-4d925c9248672e4a69a236991983375008c44997a854ee7846d1b5fd7c787788.img.xz",
|
||||
"hash": "4d925c9248672e4a69a236991983375008c44997a854ee7846d1b5fd7c787788",
|
||||
"hash_raw": "4d925c9248672e4a69a236991983375008c44997a854ee7846d1b5fd7c787788",
|
||||
"size": 184364,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "3aa0a79149ec57f4bc8c38f7bbdf4f6630dd659e49a111ce6258d2d06a07c8e5"
|
||||
},
|
||||
{
|
||||
"name": "bluetooth",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/bluetooth-9bb766d2d2ce0cc4491664b3010fe1ef62f8ffc1e362d55f78e48c4141f75533.img.xz",
|
||||
"hash": "9bb766d2d2ce0cc4491664b3010fe1ef62f8ffc1e362d55f78e48c4141f75533",
|
||||
"hash_raw": "9bb766d2d2ce0cc4491664b3010fe1ef62f8ffc1e362d55f78e48c4141f75533",
|
||||
"size": 1048576,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "9bb766d2d2ce0cc4491664b3010fe1ef62f8ffc1e362d55f78e48c4141f75533"
|
||||
},
|
||||
{
|
||||
"name": "cmnlib64",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/cmnlib64-1a876bd151bb9635f18719c4a17f953079de6e11d3eaec800968fc75669e0dc3.img.xz",
|
||||
"hash": "1a876bd151bb9635f18719c4a17f953079de6e11d3eaec800968fc75669e0dc3",
|
||||
"hash_raw": "1a876bd151bb9635f18719c4a17f953079de6e11d3eaec800968fc75669e0dc3",
|
||||
"size": 524288,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "1a876bd151bb9635f18719c4a17f953079de6e11d3eaec800968fc75669e0dc3"
|
||||
},
|
||||
{
|
||||
"name": "cmnlib",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/cmnlib-63df823e8a5fae01d66cb2b8c20f0d2ddb5c5f2425e5d0992a64676273ba1c82.img.xz",
|
||||
"hash": "63df823e8a5fae01d66cb2b8c20f0d2ddb5c5f2425e5d0992a64676273ba1c82",
|
||||
"hash_raw": "63df823e8a5fae01d66cb2b8c20f0d2ddb5c5f2425e5d0992a64676273ba1c82",
|
||||
"size": 524288,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "63df823e8a5fae01d66cb2b8c20f0d2ddb5c5f2425e5d0992a64676273ba1c82"
|
||||
},
|
||||
{
|
||||
"name": "devcfg",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/devcfg-2f374581243910db92f62bb13bd66ec8e3d56d434997ba007ded06d2d6cc8585.img.xz",
|
||||
"hash": "2f374581243910db92f62bb13bd66ec8e3d56d434997ba007ded06d2d6cc8585",
|
||||
"hash_raw": "2f374581243910db92f62bb13bd66ec8e3d56d434997ba007ded06d2d6cc8585",
|
||||
"size": 40336,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "3d7bb33588491a2a40091a7e1cf6cb65e6dd503f69b640aba484d723f1ad47e8"
|
||||
},
|
||||
{
|
||||
"name": "devinfo",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/devinfo-143869c499a7e878fbeab756e9c53074195770cc41d6d0d10e45c043141389a3.img.xz",
|
||||
"hash": "143869c499a7e878fbeab756e9c53074195770cc41d6d0d10e45c043141389a3",
|
||||
"hash_raw": "143869c499a7e878fbeab756e9c53074195770cc41d6d0d10e45c043141389a3",
|
||||
"size": 4096,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": false,
|
||||
"ondevice_hash": "143869c499a7e878fbeab756e9c53074195770cc41d6d0d10e45c043141389a3"
|
||||
},
|
||||
{
|
||||
"name": "dsp",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/dsp-4b15fbd2f45581f1553f33f01649e450b24aa19d5deff2ac7dcb16a534d9c248.img.xz",
|
||||
"hash": "4b15fbd2f45581f1553f33f01649e450b24aa19d5deff2ac7dcb16a534d9c248",
|
||||
"hash_raw": "4b15fbd2f45581f1553f33f01649e450b24aa19d5deff2ac7dcb16a534d9c248",
|
||||
"size": 33554432,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "4b15fbd2f45581f1553f33f01649e450b24aa19d5deff2ac7dcb16a534d9c248"
|
||||
},
|
||||
{
|
||||
"name": "hyp",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/hyp-ff5ece6a4e3d2b4d898c77ffe193fc8bbc8acebe78263996ecf52373d8088927.img.xz",
|
||||
"hash": "ff5ece6a4e3d2b4d898c77ffe193fc8bbc8acebe78263996ecf52373d8088927",
|
||||
"hash_raw": "ff5ece6a4e3d2b4d898c77ffe193fc8bbc8acebe78263996ecf52373d8088927",
|
||||
"size": 524288,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "ff5ece6a4e3d2b4d898c77ffe193fc8bbc8acebe78263996ecf52373d8088927"
|
||||
},
|
||||
{
|
||||
"name": "keymaster",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/keymaster-5c968c76f29b9a4d66fbe57e639bac6b7a2c83b1758e25abbaf5d276b8a6af04.img.xz",
|
||||
"hash": "5c968c76f29b9a4d66fbe57e639bac6b7a2c83b1758e25abbaf5d276b8a6af04",
|
||||
"hash_raw": "5c968c76f29b9a4d66fbe57e639bac6b7a2c83b1758e25abbaf5d276b8a6af04",
|
||||
"size": 524288,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "5c968c76f29b9a4d66fbe57e639bac6b7a2c83b1758e25abbaf5d276b8a6af04"
|
||||
},
|
||||
{
|
||||
"name": "limits",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/limits-94951a0f7aa55fb6cb975535ce4ebbfe6d695f04cb5424677b01c10dfa2e94e1.img.xz",
|
||||
"hash": "94951a0f7aa55fb6cb975535ce4ebbfe6d695f04cb5424677b01c10dfa2e94e1",
|
||||
"hash_raw": "94951a0f7aa55fb6cb975535ce4ebbfe6d695f04cb5424677b01c10dfa2e94e1",
|
||||
"size": 4096,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": false,
|
||||
"ondevice_hash": "94951a0f7aa55fb6cb975535ce4ebbfe6d695f04cb5424677b01c10dfa2e94e1"
|
||||
},
|
||||
{
|
||||
"name": "logfs",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/logfs-b8b5ac87f3d954404fc7ecbdd9ee3b5b0cf5691e5006e6ec55db4c899ff61220.img.xz",
|
||||
"hash": "b8b5ac87f3d954404fc7ecbdd9ee3b5b0cf5691e5006e6ec55db4c899ff61220",
|
||||
"hash_raw": "b8b5ac87f3d954404fc7ecbdd9ee3b5b0cf5691e5006e6ec55db4c899ff61220",
|
||||
"size": 8388608,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": false,
|
||||
"ondevice_hash": "b8b5ac87f3d954404fc7ecbdd9ee3b5b0cf5691e5006e6ec55db4c899ff61220"
|
||||
},
|
||||
{
|
||||
"name": "modem",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/modem-a3d014f0896d77a2df7e5a80a70f43a51a047b9d03cfc675b6f0e31a6ecc4994.img.xz",
|
||||
"hash": "a3d014f0896d77a2df7e5a80a70f43a51a047b9d03cfc675b6f0e31a6ecc4994",
|
||||
"hash_raw": "a3d014f0896d77a2df7e5a80a70f43a51a047b9d03cfc675b6f0e31a6ecc4994",
|
||||
"size": 125829120,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "a3d014f0896d77a2df7e5a80a70f43a51a047b9d03cfc675b6f0e31a6ecc4994"
|
||||
},
|
||||
{
|
||||
"name": "qupfw",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/qupfw-64cc7c29d5d69b04267452b8b4ddba9f4809e68f476fc162ca283f58537afe4a.img.xz",
|
||||
"hash": "64cc7c29d5d69b04267452b8b4ddba9f4809e68f476fc162ca283f58537afe4a",
|
||||
"hash_raw": "64cc7c29d5d69b04267452b8b4ddba9f4809e68f476fc162ca283f58537afe4a",
|
||||
"size": 65536,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "64cc7c29d5d69b04267452b8b4ddba9f4809e68f476fc162ca283f58537afe4a"
|
||||
},
|
||||
{
|
||||
"name": "splash",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/splash-5c61260048f22ede6e6343fabb27f6ff73f9271f4751a01aaf7abf097afc1f08.img.xz",
|
||||
"hash": "5c61260048f22ede6e6343fabb27f6ff73f9271f4751a01aaf7abf097afc1f08",
|
||||
"hash_raw": "5c61260048f22ede6e6343fabb27f6ff73f9271f4751a01aaf7abf097afc1f08",
|
||||
"size": 34226176,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": false,
|
||||
"ondevice_hash": "5c61260048f22ede6e6343fabb27f6ff73f9271f4751a01aaf7abf097afc1f08"
|
||||
},
|
||||
{
|
||||
"name": "storsec",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/storsec-4494d86f68b125fbf2c004c824b1c6dbe71e61a65d2a1cc7db13c553edcb3fce.img.xz",
|
||||
"hash": "4494d86f68b125fbf2c004c824b1c6dbe71e61a65d2a1cc7db13c553edcb3fce",
|
||||
"hash_raw": "4494d86f68b125fbf2c004c824b1c6dbe71e61a65d2a1cc7db13c553edcb3fce",
|
||||
"size": 131072,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "4494d86f68b125fbf2c004c824b1c6dbe71e61a65d2a1cc7db13c553edcb3fce"
|
||||
},
|
||||
{
|
||||
"name": "tz",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/tz-e9443bf187641661bfa6c96702b9ab0156e72fb7482500f8799ba9ee2503cb16.img.xz",
|
||||
"hash": "e9443bf187641661bfa6c96702b9ab0156e72fb7482500f8799ba9ee2503cb16",
|
||||
"hash_raw": "e9443bf187641661bfa6c96702b9ab0156e72fb7482500f8799ba9ee2503cb16",
|
||||
"size": 2097152,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "e9443bf187641661bfa6c96702b9ab0156e72fb7482500f8799ba9ee2503cb16"
|
||||
},
|
||||
{
|
||||
"name": "boot",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/boot-90bd687e9e407834d4ee1b07f3d05527dfae0ff09c0cacd64cfd6097f6b10e2c.img.xz",
|
||||
"hash": "90bd687e9e407834d4ee1b07f3d05527dfae0ff09c0cacd64cfd6097f6b10e2c",
|
||||
"hash_raw": "90bd687e9e407834d4ee1b07f3d05527dfae0ff09c0cacd64cfd6097f6b10e2c",
|
||||
"size": 17496064,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "35014c39b55010ac955c10f808b088e74259147c7a8cbf989b3dff7d95a1e8ae"
|
||||
},
|
||||
{
|
||||
"name": "system",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-d9d476b466186014e7ae4b8232bc6fc5e79b122421bdc12ff4eb02d1c3f37818.img.xz",
|
||||
"hash": "a068d4d692ec770884f0a15e1a6d7aba52385ecae138f6d43fb0a9b1643ed5cd",
|
||||
"hash_raw": "d9d476b466186014e7ae4b8232bc6fc5e79b122421bdc12ff4eb02d1c3f37818",
|
||||
"size": 4718592000,
|
||||
"sparse": true,
|
||||
"full_check": false,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "6ffa02f7113badc122742f33efebc5d17f1cd61dd6358f3e130c162707dbfaf4",
|
||||
"alt": {
|
||||
"hash": "d9d476b466186014e7ae4b8232bc6fc5e79b122421bdc12ff4eb02d1c3f37818",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-d9d476b466186014e7ae4b8232bc6fc5e79b122421bdc12ff4eb02d1c3f37818.img",
|
||||
"size": 4718592000
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "userdata_90",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/userdata_90-f9ea618ac97a86da49733ce66cd5e3aa19aa917666ee90de301cd746664e4d22.img.xz",
|
||||
"hash": "dfc6812e76bd1583ed77a86eedf48cafdc306037d2a85c5d0aa7cdb23033b736",
|
||||
"hash_raw": "f9ea618ac97a86da49733ce66cd5e3aa19aa917666ee90de301cd746664e4d22",
|
||||
"size": 96636764160,
|
||||
"sparse": true,
|
||||
"full_check": true,
|
||||
"has_ab": false,
|
||||
"ondevice_hash": "ff95f994e9ed6504632f4b7c6daecef582f0a4e5261b8240d4474f16059faef4"
|
||||
},
|
||||
{
|
||||
"name": "userdata_89",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/userdata_89-393956e255c277b895bdb98bf65cfa3907e4b57822740ff82f857ac4e1a2f11e.img.xz",
|
||||
"hash": "b5e2f05d31fc18fff18e82dcebfc2bf04de624baeca0511b93e50b3198b8a9ab",
|
||||
"hash_raw": "393956e255c277b895bdb98bf65cfa3907e4b57822740ff82f857ac4e1a2f11e",
|
||||
"size": 95563022336,
|
||||
"sparse": true,
|
||||
"full_check": true,
|
||||
"has_ab": false,
|
||||
"ondevice_hash": "db64c6abc72bfcddc1682c73cc73c7230ed2f6e835d292fd38d054a9d242b8fc"
|
||||
},
|
||||
{
|
||||
"name": "userdata_30",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/userdata_30-a4b3e2a2fc3612a37322b7b1a4c5737765841dc3b8d6d3bb58b1e5a271023068.img.xz",
|
||||
"hash": "ecec713cf7d8f1f616f122a16b138931f818290447e36a5925da6a4fc0fc7bf3",
|
||||
"hash_raw": "a4b3e2a2fc3612a37322b7b1a4c5737765841dc3b8d6d3bb58b1e5a271023068",
|
||||
"size": 32212254720,
|
||||
"sparse": true,
|
||||
"full_check": true,
|
||||
"has_ab": false,
|
||||
"ondevice_hash": "48fefa5a1880a4fd3dd50e1f9ddee297122053556816baca310d495129bc8893"
|
||||
}
|
||||
]
|
||||
Executable
+159
@@ -0,0 +1,159 @@
|
||||
#!/usr/bin/env python3
|
||||
import time
|
||||
from smbus2 import SMBus
|
||||
from collections import namedtuple
|
||||
|
||||
# https://datasheets.maximintegrated.com/en/ds/MAX98089.pdf
|
||||
|
||||
AmpConfig = namedtuple('AmpConfig', ['name', 'value', 'register', 'offset', 'mask'])
|
||||
EQParams = namedtuple('EQParams', ['K', 'k1', 'k2', 'c1', 'c2'])
|
||||
|
||||
def configs_from_eq_params(base, eq_params):
|
||||
return [
|
||||
AmpConfig("K (high)", (eq_params.K >> 8), base, 0, 0xFF),
|
||||
AmpConfig("K (low)", (eq_params.K & 0xFF), base + 1, 0, 0xFF),
|
||||
AmpConfig("k1 (high)", (eq_params.k1 >> 8), base + 2, 0, 0xFF),
|
||||
AmpConfig("k1 (low)", (eq_params.k1 & 0xFF), base + 3, 0, 0xFF),
|
||||
AmpConfig("k2 (high)", (eq_params.k2 >> 8), base + 4, 0, 0xFF),
|
||||
AmpConfig("k2 (low)", (eq_params.k2 & 0xFF), base + 5, 0, 0xFF),
|
||||
AmpConfig("c1 (high)", (eq_params.c1 >> 8), base + 6, 0, 0xFF),
|
||||
AmpConfig("c1 (low)", (eq_params.c1 & 0xFF), base + 7, 0, 0xFF),
|
||||
AmpConfig("c2 (high)", (eq_params.c2 >> 8), base + 8, 0, 0xFF),
|
||||
AmpConfig("c2 (low)", (eq_params.c2 & 0xFF), base + 9, 0, 0xFF),
|
||||
]
|
||||
|
||||
BASE_CONFIG = [
|
||||
AmpConfig("MCLK prescaler", 0b01, 0x10, 4, 0b00110000),
|
||||
AmpConfig("PM: enable speakers", 0b11, 0x4D, 4, 0b00110000),
|
||||
AmpConfig("PM: enable DACs", 0b11, 0x4D, 0, 0b00000011),
|
||||
AmpConfig("Enable PLL1", 0b1, 0x12, 7, 0b10000000),
|
||||
AmpConfig("Enable PLL2", 0b1, 0x1A, 7, 0b10000000),
|
||||
AmpConfig("DAI1: I2S mode", 0b00100, 0x14, 2, 0b01111100),
|
||||
AmpConfig("DAI2: I2S mode", 0b00100, 0x1C, 2, 0b01111100),
|
||||
AmpConfig("DAI1 Passband filtering: music mode", 0b1, 0x18, 7, 0b10000000),
|
||||
AmpConfig("DAI1 voice mode gain (DV1G)", 0b00, 0x2F, 4, 0b00110000),
|
||||
AmpConfig("DAI1 attenuation (DV1)", 0x0, 0x2F, 0, 0b00001111),
|
||||
AmpConfig("DAI2 attenuation (DV2)", 0x0, 0x31, 0, 0b00001111),
|
||||
AmpConfig("DAI2: DC blocking", 0b1, 0x20, 0, 0b00000001),
|
||||
AmpConfig("DAI2: High sample rate", 0b0, 0x20, 3, 0b00001000),
|
||||
AmpConfig("ALC enable", 0b1, 0x43, 7, 0b10000000),
|
||||
AmpConfig("ALC/excursion limiter release time", 0b101, 0x43, 4, 0b01110000),
|
||||
AmpConfig("ALC multiband enable", 0b1, 0x43, 3, 0b00001000),
|
||||
AmpConfig("DAI1 EQ enable", 0b0, 0x49, 0, 0b00000001),
|
||||
AmpConfig("DAI2 EQ clip detection disabled", 0b1, 0x32, 4, 0b00010000),
|
||||
AmpConfig("DAI2 EQ attenuation", 0x5, 0x32, 0, 0b00001111),
|
||||
AmpConfig("Excursion limiter upper corner freq", 0b100, 0x41, 4, 0b01110000),
|
||||
AmpConfig("Excursion limiter lower corner freq", 0b00, 0x41, 0, 0b00000011),
|
||||
AmpConfig("Excursion limiter threshold", 0b000, 0x42, 0, 0b00001111),
|
||||
AmpConfig("Distortion limit (THDCLP)", 0x6, 0x46, 4, 0b11110000),
|
||||
AmpConfig("Distortion limiter release time constant", 0b0, 0x46, 0, 0b00000001),
|
||||
AmpConfig("Right DAC input mixer: DAI1 left", 0b0, 0x22, 3, 0b00001000),
|
||||
AmpConfig("Right DAC input mixer: DAI1 right", 0b0, 0x22, 2, 0b00000100),
|
||||
AmpConfig("Right DAC input mixer: DAI2 left", 0b1, 0x22, 1, 0b00000010),
|
||||
AmpConfig("Right DAC input mixer: DAI2 right", 0b0, 0x22, 0, 0b00000001),
|
||||
AmpConfig("DAI1 audio port selector", 0b10, 0x16, 6, 0b11000000),
|
||||
AmpConfig("DAI2 audio port selector", 0b01, 0x1E, 6, 0b11000000),
|
||||
AmpConfig("Enable left digital microphone", 0b1, 0x48, 5, 0b00100000),
|
||||
AmpConfig("Enable right digital microphone", 0b1, 0x48, 4, 0b00010000),
|
||||
AmpConfig("Enhanced volume smoothing disabled", 0b0, 0x49, 7, 0b10000000),
|
||||
AmpConfig("Volume adjustment smoothing disabled", 0b0, 0x49, 6, 0b01000000),
|
||||
AmpConfig("Zero-crossing detection disabled", 0b0, 0x49, 5, 0b00100000),
|
||||
]
|
||||
|
||||
CONFIGS = {
|
||||
# rick - for c3
|
||||
"tici": [
|
||||
AmpConfig("Right speaker output from right DAC", 0b1, 0x2C, 0, 0b11111111),
|
||||
AmpConfig("Right Speaker Mixer Gain", 0b00, 0x2D, 2, 0b00001100),
|
||||
AmpConfig("Right speaker output volume", 0x1c, 0x3E, 0, 0b00011111),
|
||||
AmpConfig("DAI2 EQ enable", 0b1, 0x49, 1, 0b00000010),
|
||||
|
||||
*configs_from_eq_params(0x84, EQParams(0x274F, 0xC0FF, 0x3BF9, 0x0B3C, 0x1656)),
|
||||
*configs_from_eq_params(0x8E, EQParams(0x1009, 0xC6BF, 0x2952, 0x1C97, 0x30DF)),
|
||||
*configs_from_eq_params(0x98, EQParams(0x0F75, 0xCBE5, 0x0ED2, 0x2528, 0x3E42)),
|
||||
*configs_from_eq_params(0xA2, EQParams(0x091F, 0x3D4C, 0xCE11, 0x1266, 0x2807)),
|
||||
*configs_from_eq_params(0xAC, EQParams(0x0A9E, 0x3F20, 0xE573, 0x0A8B, 0x3A3B)),
|
||||
],
|
||||
"tizi": [
|
||||
AmpConfig("Left speaker output from left DAC", 0b1, 0x2B, 0, 0b11111111),
|
||||
AmpConfig("Right speaker output from right DAC", 0b1, 0x2C, 0, 0b11111111),
|
||||
AmpConfig("Left Speaker Mixer Gain", 0b00, 0x2D, 0, 0b00000011),
|
||||
AmpConfig("Right Speaker Mixer Gain", 0b00, 0x2D, 2, 0b00001100),
|
||||
AmpConfig("Left speaker output volume", 0x17, 0x3D, 0, 0b00011111),
|
||||
AmpConfig("Right speaker output volume", 0x17, 0x3E, 0, 0b00011111),
|
||||
|
||||
AmpConfig("DAI2 EQ enable", 0b0, 0x49, 1, 0b00000010),
|
||||
AmpConfig("DAI2: DC blocking", 0b0, 0x20, 0, 0b00000001),
|
||||
AmpConfig("ALC enable", 0b0, 0x43, 7, 0b10000000),
|
||||
AmpConfig("DAI2 EQ attenuation", 0x2, 0x32, 0, 0b00001111),
|
||||
AmpConfig("Excursion limiter upper corner freq", 0b001, 0x41, 4, 0b01110000),
|
||||
AmpConfig("Excursion limiter threshold", 0b100, 0x42, 0, 0b00001111),
|
||||
AmpConfig("Distortion limit (THDCLP)", 0x0, 0x46, 4, 0b11110000),
|
||||
AmpConfig("Distortion limiter release time constant", 0b1, 0x46, 0, 0b00000001),
|
||||
AmpConfig("Left DAC input mixer: DAI1 left", 0b0, 0x22, 7, 0b10000000),
|
||||
AmpConfig("Left DAC input mixer: DAI1 right", 0b0, 0x22, 6, 0b01000000),
|
||||
AmpConfig("Left DAC input mixer: DAI2 left", 0b1, 0x22, 5, 0b00100000),
|
||||
AmpConfig("Left DAC input mixer: DAI2 right", 0b0, 0x22, 4, 0b00010000),
|
||||
AmpConfig("Right DAC input mixer: DAI2 left", 0b0, 0x22, 1, 0b00000010),
|
||||
AmpConfig("Right DAC input mixer: DAI2 right", 0b1, 0x22, 0, 0b00000001),
|
||||
AmpConfig("Volume adjustment smoothing disabled", 0b1, 0x49, 6, 0b01000000),
|
||||
],
|
||||
}
|
||||
|
||||
class Amplifier:
|
||||
AMP_I2C_BUS = 0
|
||||
AMP_ADDRESS = 0x10
|
||||
|
||||
def __init__(self, debug=False):
|
||||
self.debug = debug
|
||||
|
||||
def _get_shutdown_config(self, amp_disabled: bool) -> AmpConfig:
|
||||
return AmpConfig("Global shutdown", 0b0 if amp_disabled else 0b1, 0x51, 7, 0b10000000)
|
||||
|
||||
def _set_configs(self, configs: list[AmpConfig]) -> None:
|
||||
with SMBus(self.AMP_I2C_BUS) as bus:
|
||||
for config in configs:
|
||||
if self.debug:
|
||||
print(f"Setting \"{config.name}\" to {config.value}:")
|
||||
|
||||
old_value = bus.read_byte_data(self.AMP_ADDRESS, config.register, force=True)
|
||||
new_value = (old_value & (~config.mask)) | ((config.value << config.offset) & config.mask)
|
||||
bus.write_byte_data(self.AMP_ADDRESS, config.register, new_value, force=True)
|
||||
|
||||
if self.debug:
|
||||
print(f" Changed {hex(config.register)}: {hex(old_value)} -> {hex(new_value)}")
|
||||
|
||||
def set_configs(self, configs: list[AmpConfig]) -> bool:
|
||||
# retry in case panda is using the amp
|
||||
tries = 15
|
||||
backoff = 0.
|
||||
for i in range(tries):
|
||||
try:
|
||||
self._set_configs(configs)
|
||||
return True
|
||||
except OSError:
|
||||
backoff += 0.1
|
||||
time.sleep(backoff)
|
||||
print(f"Failed to set amp config, {tries - i - 1} retries left")
|
||||
return False
|
||||
|
||||
def set_global_shutdown(self, amp_disabled: bool) -> bool:
|
||||
return self.set_configs([self._get_shutdown_config(amp_disabled), ])
|
||||
|
||||
def initialize_configuration(self, model: str) -> bool:
|
||||
cfgs = [
|
||||
self._get_shutdown_config(True),
|
||||
*BASE_CONFIG,
|
||||
*CONFIGS[model],
|
||||
self._get_shutdown_config(False),
|
||||
]
|
||||
return self.set_configs(cfgs)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
with open("/sys/firmware/devicetree/base/model") as f:
|
||||
model = f.read().strip('\x00')
|
||||
model = model.split('comma ')[-1]
|
||||
|
||||
amp = Amplifier()
|
||||
amp.initialize_configuration(model)
|
||||
@@ -0,0 +1,30 @@
|
||||
[connection]
|
||||
id=esim
|
||||
uuid=fff6553c-3284-4707-a6b1-acc021caaafb
|
||||
type=gsm
|
||||
permissions=
|
||||
autoconnect=true
|
||||
autoconnect-retries=100
|
||||
autoconnect-priority=2
|
||||
metered=1
|
||||
|
||||
[gsm]
|
||||
apn=
|
||||
home-only=false
|
||||
auto-config=true
|
||||
sim-id=
|
||||
|
||||
[ipv4]
|
||||
route-metric=1000
|
||||
dns-priority=1000
|
||||
dns-search=
|
||||
method=auto
|
||||
|
||||
[ipv6]
|
||||
ddr-gen-mode=stable-privacy
|
||||
dns-search=
|
||||
route-metric=1000
|
||||
dns-priority=1000
|
||||
method=auto
|
||||
|
||||
[proxy]
|
||||
@@ -0,0 +1,135 @@
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from typing import Literal
|
||||
|
||||
from openpilot.system.hardware.base import LPABase, LPAError, LPAProfileNotFoundError, Profile
|
||||
|
||||
class TiciLPA(LPABase):
|
||||
def __init__(self, interface: Literal['qmi', 'at'] = 'qmi'):
|
||||
self.env = os.environ.copy()
|
||||
self.env['LPAC_APDU'] = interface
|
||||
self.env['QMI_DEVICE'] = '/dev/cdc-wdm0'
|
||||
self.env['AT_DEVICE'] = '/dev/ttyUSB2'
|
||||
|
||||
self.timeout_sec = 45
|
||||
|
||||
if shutil.which('lpac') is None:
|
||||
raise LPAError('lpac not found, must be installed!')
|
||||
|
||||
def list_profiles(self) -> list[Profile]:
|
||||
msgs = self._invoke('profile', 'list')
|
||||
self._validate_successful(msgs)
|
||||
return [Profile(
|
||||
iccid=p['iccid'],
|
||||
nickname=p['profileNickname'],
|
||||
enabled=p['profileState'] == 'enabled',
|
||||
provider=p['serviceProviderName']
|
||||
) for p in msgs[-1]['payload']['data']]
|
||||
|
||||
def get_active_profile(self) -> Profile | None:
|
||||
return next((p for p in self.list_profiles() if p.enabled), None)
|
||||
|
||||
def delete_profile(self, iccid: str) -> None:
|
||||
self._validate_profile_exists(iccid)
|
||||
latest = self.get_active_profile()
|
||||
if latest is not None and latest.iccid == iccid:
|
||||
raise LPAError('cannot delete active profile, switch to another profile first')
|
||||
self._validate_successful(self._invoke('profile', 'delete', iccid))
|
||||
self._process_notifications()
|
||||
|
||||
def download_profile(self, qr: str, nickname: str | None = None) -> None:
|
||||
self._check_bootstrapped()
|
||||
msgs = self._invoke('profile', 'download', '-a', qr)
|
||||
self._validate_successful(msgs)
|
||||
new_profile = next((m for m in msgs if m['payload']['message'] == 'es8p_meatadata_parse'), None)
|
||||
if new_profile is None:
|
||||
raise LPAError('no new profile found')
|
||||
if nickname:
|
||||
self.nickname_profile(new_profile['payload']['data']['iccid'], nickname)
|
||||
self._process_notifications()
|
||||
|
||||
def nickname_profile(self, iccid: str, nickname: str) -> None:
|
||||
self._validate_profile_exists(iccid)
|
||||
self._validate_successful(self._invoke('profile', 'nickname', iccid, nickname))
|
||||
|
||||
def switch_profile(self, iccid: str) -> None:
|
||||
self._check_bootstrapped()
|
||||
self._validate_profile_exists(iccid)
|
||||
latest = self.get_active_profile()
|
||||
if latest and latest.iccid == iccid:
|
||||
return
|
||||
self._validate_successful(self._invoke('profile', 'enable', iccid))
|
||||
self._process_notifications()
|
||||
|
||||
def bootstrap(self) -> None:
|
||||
"""
|
||||
find all comma-provisioned profiles and delete them. they conflict with user-provisioned profiles
|
||||
and must be deleted.
|
||||
|
||||
**note**: this is a **very** destructive operation. you **must** purchase a new comma SIM in order
|
||||
to use comma prime again.
|
||||
"""
|
||||
if self._is_bootstrapped():
|
||||
return
|
||||
|
||||
for p in self.list_profiles():
|
||||
if self.is_comma_profile(p.iccid):
|
||||
self._disable_profile(p.iccid)
|
||||
self.delete_profile(p.iccid)
|
||||
|
||||
def _disable_profile(self, iccid: str) -> None:
|
||||
self._validate_successful(self._invoke('profile', 'disable', iccid))
|
||||
self._process_notifications()
|
||||
|
||||
def _check_bootstrapped(self) -> None:
|
||||
assert self._is_bootstrapped(), 'eUICC is not bootstrapped, please bootstrap before performing this operation'
|
||||
|
||||
def _is_bootstrapped(self) -> bool:
|
||||
""" check if any comma provisioned profiles are on the eUICC """
|
||||
return not any(self.is_comma_profile(iccid) for iccid in (p.iccid for p in self.list_profiles()))
|
||||
|
||||
def _invoke(self, *cmd: str):
|
||||
proc = subprocess.Popen(['sudo', '-E', 'lpac'] + list(cmd), stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=self.env)
|
||||
try:
|
||||
out, err = proc.communicate(timeout=self.timeout_sec)
|
||||
except subprocess.TimeoutExpired as e:
|
||||
proc.kill()
|
||||
raise LPAError(f"lpac {cmd} timed out after {self.timeout_sec} seconds") from e
|
||||
|
||||
messages = []
|
||||
for line in out.decode().strip().splitlines():
|
||||
if line.startswith('{'):
|
||||
message = json.loads(line)
|
||||
|
||||
# lpac response format validations
|
||||
assert 'type' in message, 'expected type in message'
|
||||
assert message['type'] == 'lpa' or message['type'] == 'progress', 'expected lpa or progress message type'
|
||||
assert 'payload' in message, 'expected payload in message'
|
||||
assert 'code' in message['payload'], 'expected code in message payload'
|
||||
assert 'data' in message['payload'], 'expected data in message payload'
|
||||
|
||||
msg_ret_code = message['payload']['code']
|
||||
if msg_ret_code != 0:
|
||||
raise LPAError(f"lpac {' '.join(cmd)} failed with code {msg_ret_code}: <{message['payload']['message']}> {message['payload']['data']}")
|
||||
|
||||
messages.append(message)
|
||||
|
||||
if len(messages) == 0:
|
||||
raise LPAError(f"lpac {cmd} returned no messages")
|
||||
|
||||
return messages
|
||||
|
||||
def _process_notifications(self) -> None:
|
||||
"""
|
||||
Process notifications stored on the eUICC, typically to activate/deactivate the profile with the carrier.
|
||||
"""
|
||||
self._validate_successful(self._invoke('notification', 'process', '-a', '-r'))
|
||||
|
||||
def _validate_profile_exists(self, iccid: str) -> None:
|
||||
if not any(p.iccid == iccid for p in self.list_profiles()):
|
||||
raise LPAProfileNotFoundError(f'profile {iccid} does not exist')
|
||||
|
||||
def _validate_successful(self, msgs: list[dict]) -> None:
|
||||
assert msgs[-1]['payload']['message'] == 'success', 'expected success notification'
|
||||
@@ -0,0 +1,91 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cassert>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <algorithm> // for std::clamp
|
||||
|
||||
#include "common/params.h"
|
||||
#include "common/util.h"
|
||||
#include "system/hardware/base.h"
|
||||
|
||||
class HardwareTici : public HardwareNone {
|
||||
public:
|
||||
static std::string get_name() {
|
||||
std::string model = util::read_file("/sys/firmware/devicetree/base/model");
|
||||
return util::strip(model.substr(std::string("comma ").size()));
|
||||
}
|
||||
|
||||
static cereal::InitData::DeviceType get_device_type() {
|
||||
static const std::map<std::string, cereal::InitData::DeviceType> device_map = {
|
||||
{"tici", cereal::InitData::DeviceType::TICI},
|
||||
{"tizi", cereal::InitData::DeviceType::TIZI},
|
||||
{"mici", cereal::InitData::DeviceType::MICI}
|
||||
};
|
||||
auto it = device_map.find(get_name());
|
||||
assert(it != device_map.end());
|
||||
return it->second;
|
||||
}
|
||||
|
||||
static int get_voltage() { return std::atoi(util::read_file("/sys/class/hwmon/hwmon1/in1_input").c_str()); }
|
||||
static int get_current() { return std::atoi(util::read_file("/sys/class/hwmon/hwmon1/curr1_input").c_str()); }
|
||||
|
||||
static std::string get_serial() {
|
||||
static std::string serial("");
|
||||
if (serial.empty()) {
|
||||
std::ifstream stream("/proc/cmdline");
|
||||
std::string cmdline;
|
||||
std::getline(stream, cmdline);
|
||||
|
||||
auto start = cmdline.find("serialno=");
|
||||
if (start == std::string::npos) {
|
||||
serial = "cccccc";
|
||||
} else {
|
||||
auto end = cmdline.find(" ", start + 9);
|
||||
serial = cmdline.substr(start + 9, end - start - 9);
|
||||
}
|
||||
}
|
||||
return serial;
|
||||
}
|
||||
|
||||
static void set_ir_power(int percent) {
|
||||
auto device = get_device_type();
|
||||
if (device == cereal::InitData::DeviceType::TICI ||
|
||||
device == cereal::InitData::DeviceType::TIZI) {
|
||||
return;
|
||||
}
|
||||
|
||||
int value = util::map_val(std::clamp(percent, 0, 100), 0, 100, 0, 300);
|
||||
std::ofstream("/sys/class/leds/led:switch_2/brightness") << 0 << "\n";
|
||||
std::ofstream("/sys/class/leds/led:torch_2/brightness") << value << "\n";
|
||||
std::ofstream("/sys/class/leds/led:switch_2/brightness") << value << "\n";
|
||||
}
|
||||
|
||||
static std::map<std::string, std::string> get_init_logs() {
|
||||
std::map<std::string, std::string> ret = {
|
||||
{"/BUILD", util::read_file("/BUILD")},
|
||||
{"lsblk", util::check_output("lsblk -o NAME,SIZE,STATE,VENDOR,MODEL,REV,SERIAL")},
|
||||
{"SOM ID", util::read_file("/sys/devices/platform/vendor/vendor:gpio-som-id/som_id")},
|
||||
};
|
||||
|
||||
std::string bs = util::check_output("abctl --boot_slot");
|
||||
ret["boot slot"] = bs.substr(0, bs.find_first_of("\n"));
|
||||
|
||||
std::string temp = util::read_file("/dev/disk/by-partlabel/ssd");
|
||||
temp.erase(temp.find_last_not_of(std::string("\0\r\n", 3))+1);
|
||||
ret["boot temp"] = temp;
|
||||
|
||||
// TODO: log something from system and boot
|
||||
for (std::string part : {"xbl", "abl", "aop", "devcfg", "xbl_config"}) {
|
||||
for (std::string slot : {"a", "b"}) {
|
||||
std::string partition = part + "_" + slot;
|
||||
std::string hash = util::check_output("sha256sum /dev/disk/by-partlabel/" + partition);
|
||||
ret[partition] = hash.substr(0, hash.find_first_of(" "));
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,626 @@
|
||||
import math
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
import tempfile
|
||||
from enum import IntEnum
|
||||
from functools import cached_property, lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
from cereal import log
|
||||
from openpilot.common.util import sudo_read, sudo_write
|
||||
from openpilot.common.gpio import gpio_set, gpio_init, get_irqs_for_action
|
||||
from openpilot.system.hardware.base import HardwareBase, LPABase, ThermalConfig, ThermalZone
|
||||
from openpilot.system.hardware.tici import iwlist
|
||||
from openpilot.system.hardware.tici.esim import TiciLPA
|
||||
from openpilot.system.hardware.tici.pins import GPIO
|
||||
from openpilot.system.hardware.tici.amplifier import Amplifier
|
||||
|
||||
NM = 'org.freedesktop.NetworkManager'
|
||||
NM_CON_ACT = NM + '.Connection.Active'
|
||||
NM_DEV = NM + '.Device'
|
||||
NM_DEV_WL = NM + '.Device.Wireless'
|
||||
NM_DEV_STATS = NM + '.Device.Statistics'
|
||||
NM_AP = NM + '.AccessPoint'
|
||||
DBUS_PROPS = 'org.freedesktop.DBus.Properties'
|
||||
|
||||
MM = 'org.freedesktop.ModemManager1'
|
||||
MM_MODEM = MM + ".Modem"
|
||||
MM_MODEM_SIMPLE = MM + ".Modem.Simple"
|
||||
MM_SIM = MM + ".Sim"
|
||||
|
||||
LITE = os.getenv("LITE") is not None
|
||||
|
||||
class MM_MODEM_STATE(IntEnum):
|
||||
FAILED = -1
|
||||
UNKNOWN = 0
|
||||
INITIALIZING = 1
|
||||
LOCKED = 2
|
||||
DISABLED = 3
|
||||
DISABLING = 4
|
||||
ENABLING = 5
|
||||
ENABLED = 6
|
||||
SEARCHING = 7
|
||||
REGISTERED = 8
|
||||
DISCONNECTING = 9
|
||||
CONNECTING = 10
|
||||
CONNECTED = 11
|
||||
|
||||
class NMMetered(IntEnum):
|
||||
NM_METERED_UNKNOWN = 0
|
||||
NM_METERED_YES = 1
|
||||
NM_METERED_NO = 2
|
||||
NM_METERED_GUESS_YES = 3
|
||||
NM_METERED_GUESS_NO = 4
|
||||
|
||||
TIMEOUT = 0.1
|
||||
REFRESH_RATE_MS = 1000
|
||||
|
||||
NetworkType = log.DeviceState.NetworkType
|
||||
NetworkStrength = log.DeviceState.NetworkStrength
|
||||
|
||||
# https://developer.gnome.org/ModemManager/unstable/ModemManager-Flags-and-Enumerations.html#MMModemAccessTechnology
|
||||
MM_MODEM_ACCESS_TECHNOLOGY_UMTS = 1 << 5
|
||||
MM_MODEM_ACCESS_TECHNOLOGY_LTE = 1 << 14
|
||||
|
||||
|
||||
def affine_irq(val, action):
|
||||
irqs = get_irqs_for_action(action)
|
||||
if len(irqs) == 0:
|
||||
print(f"No IRQs found for '{action}'")
|
||||
return
|
||||
|
||||
for i in irqs:
|
||||
sudo_write(str(val), f"/proc/irq/{i}/smp_affinity_list")
|
||||
|
||||
@lru_cache
|
||||
def get_device_type():
|
||||
# lru_cache and cache can cause memory leaks when used in classes
|
||||
with open("/sys/firmware/devicetree/base/model") as f:
|
||||
model = f.read().strip('\x00')
|
||||
return model.split('comma ')[-1]
|
||||
|
||||
class Tici(HardwareBase):
|
||||
@cached_property
|
||||
def bus(self):
|
||||
import dbus
|
||||
return dbus.SystemBus()
|
||||
|
||||
@cached_property
|
||||
def nm(self):
|
||||
return self.bus.get_object(NM, '/org/freedesktop/NetworkManager')
|
||||
|
||||
@property # this should not be cached, in case the modemmanager restarts
|
||||
def mm(self):
|
||||
return self.bus.get_object(MM, '/org/freedesktop/ModemManager1')
|
||||
|
||||
@cached_property
|
||||
def amplifier(self):
|
||||
if self.get_device_type() == "mici" or LITE:
|
||||
return None
|
||||
return Amplifier()
|
||||
|
||||
def get_os_version(self):
|
||||
with open("/VERSION") as f:
|
||||
return f.read().strip()
|
||||
|
||||
def get_device_type(self):
|
||||
return get_device_type()
|
||||
|
||||
def reboot(self, reason=None):
|
||||
subprocess.check_output(["sudo", "reboot"])
|
||||
|
||||
def uninstall(self):
|
||||
Path("/data/__system_reset__").touch()
|
||||
os.sync()
|
||||
self.reboot()
|
||||
|
||||
def get_serial(self):
|
||||
return self.get_cmdline()['androidboot.serialno']
|
||||
|
||||
def get_voltage(self):
|
||||
with open("/sys/class/hwmon/hwmon1/in1_input") as f:
|
||||
return int(f.read())
|
||||
|
||||
def get_current(self):
|
||||
with open("/sys/class/hwmon/hwmon1/curr1_input") as f:
|
||||
return int(f.read())
|
||||
|
||||
def set_ir_power(self, percent: int):
|
||||
if self.get_device_type() in ("tici", "tizi"):
|
||||
return
|
||||
|
||||
value = int((percent / 100) * 300)
|
||||
with open("/sys/class/leds/led:switch_2/brightness", "w") as f:
|
||||
f.write("0\n")
|
||||
with open("/sys/class/leds/led:torch_2/brightness", "w") as f:
|
||||
f.write(f"{value}\n")
|
||||
with open("/sys/class/leds/led:switch_2/brightness", "w") as f:
|
||||
f.write(f"{value}\n")
|
||||
|
||||
def get_network_type(self):
|
||||
try:
|
||||
primary_connection = self.nm.Get(NM, 'PrimaryConnection', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)
|
||||
primary_connection = self.bus.get_object(NM, primary_connection)
|
||||
primary_type = primary_connection.Get(NM_CON_ACT, 'Type', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)
|
||||
|
||||
if primary_type == '802-3-ethernet':
|
||||
return NetworkType.ethernet
|
||||
elif primary_type == '802-11-wireless':
|
||||
return NetworkType.wifi
|
||||
else:
|
||||
active_connections = self.nm.Get(NM, 'ActiveConnections', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)
|
||||
for conn in active_connections:
|
||||
c = self.bus.get_object(NM, conn)
|
||||
tp = c.Get(NM_CON_ACT, 'Type', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)
|
||||
if tp == 'gsm':
|
||||
modem = self.get_modem()
|
||||
access_t = modem.Get(MM_MODEM, 'AccessTechnologies', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)
|
||||
if access_t >= MM_MODEM_ACCESS_TECHNOLOGY_LTE:
|
||||
return NetworkType.cell4G
|
||||
elif access_t >= MM_MODEM_ACCESS_TECHNOLOGY_UMTS:
|
||||
return NetworkType.cell3G
|
||||
else:
|
||||
return NetworkType.cell2G
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return NetworkType.none
|
||||
|
||||
def get_modem(self):
|
||||
objects = self.mm.GetManagedObjects(dbus_interface="org.freedesktop.DBus.ObjectManager", timeout=TIMEOUT)
|
||||
modem_path = list(objects.keys())[0]
|
||||
return self.bus.get_object(MM, modem_path)
|
||||
|
||||
def get_wlan(self):
|
||||
wlan_path = self.nm.GetDeviceByIpIface('wlan0', dbus_interface=NM, timeout=TIMEOUT)
|
||||
return self.bus.get_object(NM, wlan_path)
|
||||
|
||||
def get_wwan(self):
|
||||
wwan_path = self.nm.GetDeviceByIpIface('wwan0', dbus_interface=NM, timeout=TIMEOUT)
|
||||
return self.bus.get_object(NM, wwan_path)
|
||||
|
||||
def get_sim_info(self):
|
||||
modem = self.get_modem()
|
||||
sim_path = modem.Get(MM_MODEM, 'Sim', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)
|
||||
|
||||
if sim_path == "/":
|
||||
return {
|
||||
'sim_id': '',
|
||||
'mcc_mnc': None,
|
||||
'network_type': ["Unknown"],
|
||||
'sim_state': ["ABSENT"],
|
||||
'data_connected': False
|
||||
}
|
||||
else:
|
||||
sim = self.bus.get_object(MM, sim_path)
|
||||
return {
|
||||
'sim_id': str(sim.Get(MM_SIM, 'SimIdentifier', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)),
|
||||
'mcc_mnc': str(sim.Get(MM_SIM, 'OperatorIdentifier', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)),
|
||||
'network_type': ["Unknown"],
|
||||
'sim_state': ["READY"],
|
||||
'data_connected': modem.Get(MM_MODEM, 'State', dbus_interface=DBUS_PROPS, timeout=TIMEOUT) == MM_MODEM_STATE.CONNECTED,
|
||||
}
|
||||
|
||||
def get_sim_lpa(self) -> LPABase:
|
||||
return TiciLPA()
|
||||
|
||||
def get_imei(self, slot):
|
||||
if slot != 0:
|
||||
return ""
|
||||
|
||||
return str(self.get_modem().Get(MM_MODEM, 'EquipmentIdentifier', dbus_interface=DBUS_PROPS, timeout=TIMEOUT))
|
||||
|
||||
def get_network_info(self):
|
||||
if self.get_device_type() == "mici" or LITE:
|
||||
return None
|
||||
try:
|
||||
modem = self.get_modem()
|
||||
info = modem.Command("AT+QNWINFO", math.ceil(TIMEOUT), dbus_interface=MM_MODEM, timeout=TIMEOUT)
|
||||
extra = modem.Command('AT+QENG="servingcell"', math.ceil(TIMEOUT), dbus_interface=MM_MODEM, timeout=TIMEOUT)
|
||||
state = modem.Get(MM_MODEM, 'State', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
if info and info.startswith('+QNWINFO: '):
|
||||
info = info.replace('+QNWINFO: ', '').replace('"', '').split(',')
|
||||
extra = "" if extra is None else extra.replace('+QENG: "servingcell",', '').replace('"', '')
|
||||
state = "" if state is None else MM_MODEM_STATE(state).name
|
||||
|
||||
if len(info) != 4:
|
||||
return None
|
||||
|
||||
technology, operator, band, channel = info
|
||||
|
||||
return({
|
||||
'technology': technology,
|
||||
'operator': operator,
|
||||
'band': band,
|
||||
'channel': int(channel),
|
||||
'extra': extra,
|
||||
'state': state,
|
||||
})
|
||||
else:
|
||||
return None
|
||||
|
||||
def parse_strength(self, percentage):
|
||||
if percentage < 25:
|
||||
return NetworkStrength.poor
|
||||
elif percentage < 50:
|
||||
return NetworkStrength.moderate
|
||||
elif percentage < 75:
|
||||
return NetworkStrength.good
|
||||
else:
|
||||
return NetworkStrength.great
|
||||
|
||||
def get_network_strength(self, network_type):
|
||||
network_strength = NetworkStrength.unknown
|
||||
|
||||
try:
|
||||
if network_type == NetworkType.none:
|
||||
pass
|
||||
elif network_type == NetworkType.wifi:
|
||||
wlan = self.get_wlan()
|
||||
active_ap_path = wlan.Get(NM_DEV_WL, 'ActiveAccessPoint', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)
|
||||
if active_ap_path != "/":
|
||||
active_ap = self.bus.get_object(NM, active_ap_path)
|
||||
strength = int(active_ap.Get(NM_AP, 'Strength', dbus_interface=DBUS_PROPS, timeout=TIMEOUT))
|
||||
network_strength = self.parse_strength(strength)
|
||||
else: # Cellular
|
||||
modem = self.get_modem()
|
||||
strength = int(modem.Get(MM_MODEM, 'SignalQuality', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)[0])
|
||||
network_strength = self.parse_strength(strength)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return network_strength
|
||||
|
||||
def get_network_metered(self, network_type) -> bool:
|
||||
try:
|
||||
primary_connection = self.nm.Get(NM, 'PrimaryConnection', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)
|
||||
primary_connection = self.bus.get_object(NM, primary_connection)
|
||||
primary_devices = primary_connection.Get(NM_CON_ACT, 'Devices', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)
|
||||
|
||||
for dev in primary_devices:
|
||||
dev_obj = self.bus.get_object(NM, str(dev))
|
||||
metered_prop = dev_obj.Get(NM_DEV, 'Metered', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)
|
||||
|
||||
if network_type == NetworkType.wifi:
|
||||
if metered_prop in [NMMetered.NM_METERED_YES, NMMetered.NM_METERED_GUESS_YES]:
|
||||
return True
|
||||
elif network_type in [NetworkType.cell2G, NetworkType.cell3G, NetworkType.cell4G, NetworkType.cell5G]:
|
||||
if metered_prop == NMMetered.NM_METERED_NO:
|
||||
return False
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return super().get_network_metered(network_type)
|
||||
|
||||
def get_modem_version(self):
|
||||
try:
|
||||
modem = self.get_modem()
|
||||
return modem.Get(MM_MODEM, 'Revision', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_modem_temperatures(self):
|
||||
if LITE:
|
||||
return []
|
||||
timeout = 0.2 # Default timeout is too short
|
||||
try:
|
||||
modem = self.get_modem()
|
||||
temps = modem.Command("AT+QTEMP", math.ceil(timeout), dbus_interface=MM_MODEM, timeout=timeout)
|
||||
return list(filter(lambda t: t != 255, map(int, temps.split(' ')[1].split(','))))
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def get_current_power_draw(self):
|
||||
return (self.read_param_file("/sys/class/hwmon/hwmon1/power1_input", int) / 1e6)
|
||||
|
||||
def get_som_power_draw(self):
|
||||
return (self.read_param_file("/sys/class/power_supply/bms/voltage_now", int) * self.read_param_file("/sys/class/power_supply/bms/current_now", int) / 1e12)
|
||||
|
||||
def shutdown(self):
|
||||
os.system("sudo poweroff")
|
||||
|
||||
def get_thermal_config(self):
|
||||
intake, exhaust, case = None, None, None
|
||||
if self.get_device_type() == "mici":
|
||||
case = ThermalZone("case")
|
||||
intake = ThermalZone("intake")
|
||||
exhaust = ThermalZone("exhaust")
|
||||
return ThermalConfig(cpu=[ThermalZone(f"cpu{i}-silver-usr") for i in range(4)] +
|
||||
[ThermalZone(f"cpu{i}-gold-usr") for i in range(4)],
|
||||
gpu=[ThermalZone("gpu0-usr"), ThermalZone("gpu1-usr")],
|
||||
dsp=ThermalZone("compute-hvx-usr"),
|
||||
memory=ThermalZone("ddr-usr"),
|
||||
pmic=[ThermalZone("pm8998_tz"), ThermalZone("pm8005_tz")],
|
||||
intake=intake,
|
||||
exhaust=exhaust,
|
||||
case=case)
|
||||
|
||||
def set_display_power(self, on):
|
||||
try:
|
||||
with open("/sys/class/backlight/panel0-backlight/bl_power", "w") as f:
|
||||
f.write("0" if on else "4")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def set_screen_brightness(self, percentage):
|
||||
try:
|
||||
with open("/sys/class/backlight/panel0-backlight/max_brightness") as f:
|
||||
max_brightness = float(f.read().strip())
|
||||
|
||||
val = int(percentage * (max_brightness / 100.))
|
||||
with open("/sys/class/backlight/panel0-backlight/brightness", "w") as f:
|
||||
f.write(str(val))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def get_screen_brightness(self):
|
||||
try:
|
||||
with open("/sys/class/backlight/panel0-backlight/max_brightness") as f:
|
||||
max_brightness = float(f.read().strip())
|
||||
|
||||
with open("/sys/class/backlight/panel0-backlight/brightness") as f:
|
||||
return int(float(f.read()) / (max_brightness / 100.))
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
def set_power_save(self, powersave_enabled):
|
||||
# amplifier, 100mW at idle
|
||||
if self.amplifier is not None:
|
||||
self.amplifier.set_global_shutdown(amp_disabled=powersave_enabled)
|
||||
if not powersave_enabled:
|
||||
self.amplifier.initialize_configuration(self.get_device_type())
|
||||
|
||||
# *** CPU config ***
|
||||
|
||||
# offline big cluster
|
||||
for i in range(4, 8):
|
||||
val = '0' if powersave_enabled else '1'
|
||||
sudo_write(val, f'/sys/devices/system/cpu/cpu{i}/online')
|
||||
|
||||
for n in ('0', '4'):
|
||||
if powersave_enabled and n == '4':
|
||||
continue
|
||||
gov = 'ondemand' if powersave_enabled else 'performance'
|
||||
sudo_write(gov, f'/sys/devices/system/cpu/cpufreq/policy{n}/scaling_governor')
|
||||
|
||||
# *** IRQ config ***
|
||||
|
||||
# GPU, modeld core
|
||||
affine_irq(7, "kgsl-3d0")
|
||||
|
||||
# camerad core
|
||||
camera_irqs = ("a5", "cci", "cpas_camnoc", "cpas-cdm", "csid", "ife", "csid-lite", "ife-lite")
|
||||
for n in camera_irqs:
|
||||
affine_irq(6, n)
|
||||
|
||||
def get_gpu_usage_percent(self):
|
||||
try:
|
||||
with open('/sys/class/kgsl/kgsl-3d0/gpubusy') as f:
|
||||
used, total = f.read().strip().split()
|
||||
return 100.0 * int(used) / int(total)
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
def initialize_hardware(self):
|
||||
if self.amplifier is not None:
|
||||
self.amplifier.initialize_configuration(self.get_device_type())
|
||||
|
||||
# Allow hardwared to write engagement status to kmsg
|
||||
os.system("sudo chmod a+w /dev/kmsg")
|
||||
|
||||
# Ensure fan gpio is enabled so fan runs until shutdown, also turned on at boot by the ABL
|
||||
gpio_init(GPIO.SOM_ST_IO, True)
|
||||
gpio_set(GPIO.SOM_ST_IO, 1)
|
||||
|
||||
# *** IRQ config ***
|
||||
|
||||
# mask off big cluster from default affinity
|
||||
sudo_write("f", "/proc/irq/default_smp_affinity")
|
||||
|
||||
# move these off the default core
|
||||
affine_irq(1, "msm_vidc") # encoders
|
||||
affine_irq(1, "i2c_geni") # sensors
|
||||
|
||||
# *** GPU config ***
|
||||
# https://github.com/commaai/agnos-kernel-sdm845/blob/master/arch/arm64/boot/dts/qcom/sdm845-gpu.dtsi#L216
|
||||
affine_irq(5, "fts_ts") # touch
|
||||
affine_irq(5, "msm_drm") # display
|
||||
sudo_write("1", "/sys/class/kgsl/kgsl-3d0/min_pwrlevel")
|
||||
sudo_write("1", "/sys/class/kgsl/kgsl-3d0/max_pwrlevel")
|
||||
sudo_write("1", "/sys/class/kgsl/kgsl-3d0/force_bus_on")
|
||||
sudo_write("1", "/sys/class/kgsl/kgsl-3d0/force_clk_on")
|
||||
sudo_write("1", "/sys/class/kgsl/kgsl-3d0/force_rail_on")
|
||||
sudo_write("1000", "/sys/class/kgsl/kgsl-3d0/idle_timer")
|
||||
sudo_write("performance", "/sys/class/kgsl/kgsl-3d0/devfreq/governor")
|
||||
sudo_write("710", "/sys/class/kgsl/kgsl-3d0/max_clock_mhz")
|
||||
|
||||
# setup governors
|
||||
sudo_write("performance", "/sys/class/devfreq/soc:qcom,cpubw/governor")
|
||||
sudo_write("performance", "/sys/class/devfreq/soc:qcom,memlat-cpu0/governor")
|
||||
sudo_write("performance", "/sys/class/devfreq/soc:qcom,memlat-cpu4/governor")
|
||||
|
||||
# *** VIDC (encoder) config ***
|
||||
sudo_write("N", "/sys/kernel/debug/msm_vidc/clock_scaling")
|
||||
sudo_write("Y", "/sys/kernel/debug/msm_vidc/disable_thermal_mitigation")
|
||||
|
||||
# pandad core
|
||||
affine_irq(3, "spi_geni") # SPI
|
||||
# rick - for c3
|
||||
if "tici" in self.get_device_type():
|
||||
affine_irq(3, "xhci-hcd:usb3") # aux panda USB (or potentially anything else on USB)
|
||||
affine_irq(3, "xhci-hcd:usb1") # internal panda USB (also modem)
|
||||
try:
|
||||
pid = subprocess.check_output(["pgrep", "-f", "spi0"], encoding='utf8').strip()
|
||||
subprocess.call(["sudo", "chrt", "-f", "-p", "1", pid])
|
||||
subprocess.call(["sudo", "taskset", "-pc", "3", pid])
|
||||
except subprocess.CalledProcessException as e:
|
||||
print(str(e))
|
||||
|
||||
def configure_modem(self):
|
||||
sim_id = self.get_sim_info().get('sim_id', '')
|
||||
|
||||
modem = self.get_modem()
|
||||
try:
|
||||
manufacturer = str(modem.Get(MM_MODEM, 'Manufacturer', dbus_interface=DBUS_PROPS, timeout=TIMEOUT))
|
||||
except Exception:
|
||||
manufacturer = None
|
||||
|
||||
cmds = []
|
||||
|
||||
# rick - for c3
|
||||
if self.get_device_type() in ("tizi", "tici"):
|
||||
# clear out old blue prime initial APN
|
||||
os.system('mmcli -m any --3gpp-set-initial-eps-bearer-settings="apn="')
|
||||
|
||||
# rick - c3, only for tizi
|
||||
if self.get_device_type() == "tizi":
|
||||
cmds += [
|
||||
# SIM hot swap
|
||||
'AT+QSIMDET=1,0',
|
||||
'AT+QSIMSTAT=1',
|
||||
]
|
||||
|
||||
cmds += [
|
||||
# rick - move away, not for c3
|
||||
# # SIM hot swap
|
||||
# 'AT+QSIMDET=1,0',
|
||||
# 'AT+QSIMSTAT=1',
|
||||
|
||||
# configure modem as data-centric
|
||||
'AT+QNVW=5280,0,"0102000000000000"',
|
||||
'AT+QNVFW="/nv/item_files/ims/IMS_enable",00',
|
||||
'AT+QNVFW="/nv/item_files/modem/mmode/ue_usage_setting",01',
|
||||
]
|
||||
elif manufacturer == 'Cavli Inc.':
|
||||
cmds += [
|
||||
'AT^SIMSWAP=1', # use SIM slot, instead of internal eSIM
|
||||
'AT$QCSIMSLEEP=0', # disable SIM sleep
|
||||
'AT$QCSIMCFG=SimPowerSave,0', # more sleep disable
|
||||
|
||||
# ethernet config
|
||||
'AT$QCPCFG=usbNet,0',
|
||||
'AT$QCNETDEVCTL=3,1',
|
||||
]
|
||||
else:
|
||||
# this modem gets upset with too many AT commands
|
||||
if sim_id is None or len(sim_id) == 0:
|
||||
cmds += [
|
||||
# SIM sleep disable
|
||||
'AT$QCSIMSLEEP=0',
|
||||
'AT$QCSIMCFG=SimPowerSave,0',
|
||||
|
||||
# ethernet config
|
||||
'AT$QCPCFG=usbNet,1',
|
||||
]
|
||||
|
||||
for cmd in cmds:
|
||||
try:
|
||||
modem.Command(cmd, math.ceil(TIMEOUT), dbus_interface=MM_MODEM, timeout=TIMEOUT)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# eSIM prime
|
||||
dest = "/etc/NetworkManager/system-connections/esim.nmconnection"
|
||||
if self.get_sim_lpa().is_comma_profile(sim_id) and not os.path.exists(dest):
|
||||
with open(Path(__file__).parent/'esim.nmconnection') as f, tempfile.NamedTemporaryFile(mode='w') as tf:
|
||||
dat = f.read()
|
||||
dat = dat.replace("sim-id=", f"sim-id={sim_id}")
|
||||
tf.write(dat)
|
||||
tf.flush()
|
||||
|
||||
# needs to be root
|
||||
os.system(f"sudo cp {tf.name} {dest}")
|
||||
os.system(f"sudo nmcli con load {dest}")
|
||||
|
||||
def reboot_modem(self):
|
||||
modem = self.get_modem()
|
||||
for state in (0, 1):
|
||||
try:
|
||||
modem.Command(f'AT+CFUN={state}', math.ceil(TIMEOUT), dbus_interface=MM_MODEM, timeout=TIMEOUT)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def get_networks(self):
|
||||
r = {}
|
||||
|
||||
wlan = iwlist.scan()
|
||||
if wlan is not None:
|
||||
r['wlan'] = wlan
|
||||
|
||||
lte_info = self.get_network_info()
|
||||
if lte_info is not None:
|
||||
extra = lte_info['extra']
|
||||
|
||||
# <state>,"LTE",<is_tdd>,<mcc>,<mnc>,<cellid>,<pcid>,<earfcn>,<freq_band_ind>,
|
||||
# <ul_bandwidth>,<dl_bandwidth>,<tac>,<rsrp>,<rsrq>,<rssi>,<sinr>,<srxlev>
|
||||
if 'LTE' in extra:
|
||||
extra = extra.split(',')
|
||||
try:
|
||||
r['lte'] = [{
|
||||
"mcc": int(extra[3]),
|
||||
"mnc": int(extra[4]),
|
||||
"cid": int(extra[5], 16),
|
||||
"nmr": [{"pci": int(extra[6]), "earfcn": int(extra[7])}],
|
||||
}]
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
|
||||
return r
|
||||
|
||||
def get_modem_data_usage(self):
|
||||
try:
|
||||
wwan = self.get_wwan()
|
||||
|
||||
# Ensure refresh rate is set so values don't go stale
|
||||
refresh_rate = wwan.Get(NM_DEV_STATS, 'RefreshRateMs', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)
|
||||
if refresh_rate != REFRESH_RATE_MS:
|
||||
u = type(refresh_rate)
|
||||
wwan.Set(NM_DEV_STATS, 'RefreshRateMs', u(REFRESH_RATE_MS), dbus_interface=DBUS_PROPS, timeout=TIMEOUT)
|
||||
|
||||
tx = wwan.Get(NM_DEV_STATS, 'TxBytes', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)
|
||||
rx = wwan.Get(NM_DEV_STATS, 'RxBytes', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)
|
||||
return int(tx), int(rx)
|
||||
except Exception:
|
||||
return -1, -1
|
||||
|
||||
def has_internal_panda(self):
|
||||
return True
|
||||
|
||||
def reset_internal_panda(self):
|
||||
gpio_init(GPIO.STM_RST_N, True)
|
||||
gpio_init(GPIO.STM_BOOT0, True)
|
||||
|
||||
gpio_set(GPIO.STM_RST_N, 1)
|
||||
gpio_set(GPIO.STM_BOOT0, 0)
|
||||
time.sleep(1)
|
||||
gpio_set(GPIO.STM_RST_N, 0)
|
||||
|
||||
def recover_internal_panda(self):
|
||||
gpio_init(GPIO.STM_RST_N, True)
|
||||
gpio_init(GPIO.STM_BOOT0, True)
|
||||
|
||||
gpio_set(GPIO.STM_RST_N, 1)
|
||||
gpio_set(GPIO.STM_BOOT0, 1)
|
||||
time.sleep(0.5)
|
||||
gpio_set(GPIO.STM_RST_N, 0)
|
||||
time.sleep(0.5)
|
||||
gpio_set(GPIO.STM_BOOT0, 0)
|
||||
|
||||
def booted(self):
|
||||
# this normally boots within 8s, but on rare occasions takes 30+s
|
||||
encoder_state = sudo_read("/sys/kernel/debug/msm_vidc/core0/info")
|
||||
if "Core state: 0" in encoder_state and (time.monotonic() < 60*2):
|
||||
return False
|
||||
return True
|
||||
|
||||
if __name__ == "__main__":
|
||||
t = Tici()
|
||||
t.configure_modem()
|
||||
t.initialize_hardware()
|
||||
t.set_power_save(False)
|
||||
print(t.get_sim_info())
|
||||
@@ -0,0 +1,28 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC+iXXq30Tq+J5N
|
||||
Kat3KWHCzcmwZ55nGh6WggAqECa5CasBlM9VeROpVu3beA+5h0MibRgbD4DMtVXB
|
||||
t6gEvZ8nd04E7eLA9LTZyFDZ7SkSOVj4oXOQsT0GnJmKrASW5KslTWqVzTfo2XCt
|
||||
Z+004ikLxmyFeBO8NOcErW1pa8gFdQDToH9FrA7kgysic/XVESTOoe7XlzRoe/eZ
|
||||
acEQ+jtnmFd21A4aEADkk00Ahjr0uKaJiLUAPatxs2icIXWpgYtfqqtaKF23wSt6
|
||||
1OTu6cAwXbOWr3m+IUSRUO0IRzEIQS3z1jfd1svgzSgSSwZ1Lhj4AoKxIEAIc8qJ
|
||||
rO4uymCJAgMBAAECggEBAISFevxHGdoL3Z5xkw6oO5SQKO2GxEeVhRzNgmu/HA+q
|
||||
x8OryqD6O1CWY4037kft6iWxlwiLOdwna2P25ueVM3LxqdQH2KS4DmlCx+kq6FwC
|
||||
gv063fQPMhC9LpWimvaQSPEC7VUPjQlo4tPY6sTTYBUOh0A1ihRm/x7juKuQCWix
|
||||
Cq8C/DVnB1X4mGj+W3nJc5TwVJtgJbbiBrq6PWrhvB/3qmkxHRL7dU2SBb2iNRF1
|
||||
LLY30dJx/cD73UDKNHrlrsjk3UJc29Mp4/MladKvUkRqNwlYxSuAtJV0nZ3+iFkL
|
||||
s3adSTHdJpClQer45R51rFDlVsDz2ZBpb/hRNRoGDuECgYEA6A1EixLq7QYOh3cb
|
||||
Xhyh3W4kpVvA/FPfKH1OMy3ONOD/Y9Oa+M/wthW1wSoRL2n+uuIW5OAhTIvIEivj
|
||||
6bAZsTT3twrvOrvYu9rx9aln4p8BhyvdjeW4kS7T8FP5ol6LoOt2sTP3T1LOuJPO
|
||||
uQvOjlKPKIMh3c3RFNWTnGzMPa0CgYEA0jNiPLxP3A2nrX0keKDI+VHuvOY88gdh
|
||||
0W5BuLMLovOIDk9aQFIbBbMuW1OTjHKv9NK+Lrw+YbCFqOGf1dU/UN5gSyE8lX/Q
|
||||
FsUGUqUZx574nJZnOIcy3ONOnQLcvHAQToLFAGUd7PWgP3CtHkt9hEv2koUwL4vo
|
||||
ikTP1u9Gkc0CgYEA2apoWxPZrY963XLKBxNQecYxNbLFaWq67t3rFnKm9E8BAICi
|
||||
4zUaE5J1tMVi7Vi9iks9Ml9SnNyZRQJKfQ+kaebHXbkyAaPmfv+26rqHKboA0uxA
|
||||
nDOZVwXX45zBkp6g1sdHxJx8JLoGEnkC9eyvSi0C//tRLx86OhLErXwYcNkCf1it
|
||||
VMRKrWYoXJTUNo6tRhvodM88UnnIo3u3CALjhgU4uC1RTMHV4ZCGBwiAOb8GozSl
|
||||
s5YD1E1iKwEULloHnK6BIh6P5v8q7J6uf/xdqoKMjlWBHgq6/roxKvkSPA1DOZ3l
|
||||
jTadcgKFnRUmc+JT9p/ZbCxkA/ALFg8++G+0ghECgYA8vG3M/utweLvq4RI7l7U7
|
||||
b+i2BajfK2OmzNi/xugfeLjY6k2tfQGRuv6ppTjehtji2uvgDWkgjJUgPfZpir3I
|
||||
RsVMUiFgloWGHETOy0Qvc5AwtqTJFLTD1Wza2uBilSVIEsg6Y83Gickh+ejOmEsY
|
||||
6co17RFaAZHwGfCFFjO76Q==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
@@ -0,0 +1,35 @@
|
||||
import subprocess
|
||||
|
||||
|
||||
def scan(interface="wlan0"):
|
||||
result = []
|
||||
try:
|
||||
r = subprocess.check_output(["iwlist", interface, "scan"], encoding='utf8')
|
||||
|
||||
mac = None
|
||||
for line in r.split('\n'):
|
||||
if "Address" in line:
|
||||
# Based on the adapter eithere a percentage or dBm is returned
|
||||
# Add previous network in case no dBm signal level was seen
|
||||
if mac is not None:
|
||||
result.append({"mac": mac})
|
||||
mac = None
|
||||
|
||||
mac = line.split(' ')[-1]
|
||||
elif "dBm" in line:
|
||||
try:
|
||||
level = line.split('Signal level=')[1]
|
||||
rss = int(level.split(' ')[0])
|
||||
result.append({"mac": mac, "rss": rss})
|
||||
mac = None
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
# Add last network if no dBm was found
|
||||
if mac is not None:
|
||||
result.append({"mac": mac})
|
||||
|
||||
return result
|
||||
|
||||
except Exception:
|
||||
return None
|
||||
@@ -0,0 +1,32 @@
|
||||
# GPIO pin definitions
|
||||
class GPIO:
|
||||
# both GPIO_STM_RST_N and GPIO_LTE_RST_N are misnamed, they are high to reset
|
||||
HUB_RST_N = 30
|
||||
UBLOX_RST_N = 32
|
||||
UBLOX_SAFEBOOT_N = 33
|
||||
GNSS_PWR_EN = 34 # SCHEMATIC LABEL: GPIO_UBLOX_PWR_EN
|
||||
|
||||
STM_RST_N = 124
|
||||
STM_BOOT0 = 134
|
||||
STM_PWR_EN_N = 41 # because STM32H7 RST doesn't generate a full power-on-reset
|
||||
|
||||
SIREN = 42
|
||||
SOM_ST_IO = 49
|
||||
|
||||
LTE_RST_N = 50
|
||||
LTE_PWRKEY = 116
|
||||
LTE_BOOT = 52
|
||||
|
||||
# GPIO_CAM0_DVDD_EN = /sys/kernel/debug/regulator/camera_rear_ldo
|
||||
CAM0_AVDD_EN = 8
|
||||
CAM0_RSTN = 9
|
||||
CAM1_RSTN = 7
|
||||
CAM2_RSTN = 12
|
||||
|
||||
# Sensor interrupts
|
||||
LSM_INT = 84
|
||||
|
||||
# rick - for c3
|
||||
BMX055_ACCEL_INT = 21
|
||||
BMX055_GYRO_INT = 23
|
||||
BMX055_MAGN_INT = 87
|
||||
Executable
+66
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import time
|
||||
import datetime
|
||||
import numpy as np
|
||||
from collections import deque
|
||||
|
||||
from openpilot.common.realtime import Ratekeeper
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
|
||||
|
||||
def read_power():
|
||||
with open("/sys/bus/i2c/devices/0-0040/hwmon/hwmon1/power1_input") as f:
|
||||
return int(f.read()) / 1e6
|
||||
|
||||
def sample_power(seconds=5) -> list[float]:
|
||||
rate = 123
|
||||
rk = Ratekeeper(rate, print_delay_threshold=None)
|
||||
|
||||
pwrs = []
|
||||
for _ in range(rate*seconds):
|
||||
pwrs.append(read_power())
|
||||
rk.keep_time()
|
||||
return pwrs
|
||||
|
||||
def get_power(seconds=5):
|
||||
pwrs = sample_power(seconds)
|
||||
return np.mean(pwrs)
|
||||
|
||||
def wait_for_power(min_pwr, max_pwr, min_secs_in_range, timeout):
|
||||
start_time = time.monotonic()
|
||||
pwrs = deque([min_pwr - 1.]*min_secs_in_range, maxlen=min_secs_in_range)
|
||||
while (time.monotonic() - start_time < timeout):
|
||||
pwrs.append(get_power(1))
|
||||
if all(min_pwr <= p <= max_pwr for p in pwrs):
|
||||
break
|
||||
return np.mean(pwrs)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
duration = None
|
||||
if len(sys.argv) > 1:
|
||||
duration = int(sys.argv[1])
|
||||
|
||||
rate = 23
|
||||
rk = Ratekeeper(rate, print_delay_threshold=None)
|
||||
fltr = FirstOrderFilter(0, 5, 1. / rate, initialized=False)
|
||||
|
||||
measurements = []
|
||||
start_time = time.monotonic()
|
||||
|
||||
try:
|
||||
while duration is None or time.monotonic() - start_time < duration:
|
||||
fltr.update(read_power())
|
||||
if rk.frame % rate == 0:
|
||||
measurements.append(fltr.x)
|
||||
t = datetime.timedelta(seconds=time.monotonic() - start_time)
|
||||
avg = sum(measurements) / len(measurements)
|
||||
print(f"Now: {fltr.x:.2f} W, Avg: {avg:.2f} W over {t}")
|
||||
rk.keep_time()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
t = datetime.timedelta(seconds=time.monotonic() - start_time)
|
||||
avg = sum(measurements) / len(measurements)
|
||||
print(f"\nAverage power: {avg:.2f}W over {t}")
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
import numpy as np
|
||||
from openpilot.system.hardware.tici.power_monitor import sample_power
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("measuring for 5 seconds")
|
||||
for _ in range(3):
|
||||
pwrs = sample_power()
|
||||
print(f"mean {np.mean(pwrs):.2f} std {np.std(pwrs):.2f}")
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
#nmcli connection modify --temporary lte gsm.home-only yes
|
||||
#nmcli connection modify --temporary lte gsm.auto-config yes
|
||||
#nmcli connection modify --temporary lte connection.autoconnect-retries 20
|
||||
sudo nmcli connection reload
|
||||
|
||||
sudo systemctl stop ModemManager
|
||||
nmcli con down lte
|
||||
nmcli con down blue-prime
|
||||
|
||||
# power cycle modem
|
||||
/usr/comma/lte/lte.sh stop_blocking
|
||||
/usr/comma/lte/lte.sh start
|
||||
|
||||
sudo systemctl restart NetworkManager
|
||||
#sudo systemctl restart ModemManager
|
||||
sudo ModemManager --debug
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import collections
|
||||
import multiprocessing
|
||||
import os
|
||||
|
||||
import requests
|
||||
from tqdm import tqdm
|
||||
|
||||
import openpilot.system.hardware.tici.casync as casync
|
||||
|
||||
|
||||
def get_chunk_download_size(chunk):
|
||||
sha = chunk.sha.hex()
|
||||
path = os.path.join(remote_url, sha[:4], sha + ".cacnk")
|
||||
if os.path.isfile(path):
|
||||
return os.path.getsize(path)
|
||||
else:
|
||||
r = requests.head(path, timeout=10)
|
||||
r.raise_for_status()
|
||||
return int(r.headers['content-length'])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
parser = argparse.ArgumentParser(description='Compute overlap between two casync manifests')
|
||||
parser.add_argument('frm')
|
||||
parser.add_argument('to')
|
||||
args = parser.parse_args()
|
||||
|
||||
frm = casync.parse_caibx(args.frm)
|
||||
to = casync.parse_caibx(args.to)
|
||||
remote_url = args.to.replace('.caibx', '')
|
||||
|
||||
most_common = collections.Counter(t.sha for t in to).most_common(1)[0][0]
|
||||
|
||||
frm_dict = casync.build_chunk_dict(frm)
|
||||
|
||||
# Get content-length for each chunk
|
||||
with multiprocessing.Pool() as pool:
|
||||
szs = list(tqdm(pool.imap(get_chunk_download_size, to), total=len(to)))
|
||||
chunk_sizes = {t.sha: sz for (t, sz) in zip(to, szs, strict=True)}
|
||||
|
||||
sources: dict[str, list[int]] = {
|
||||
'seed': [],
|
||||
'remote_uncompressed': [],
|
||||
'remote_compressed': [],
|
||||
}
|
||||
|
||||
for chunk in to:
|
||||
# Assume most common chunk is the zero chunk
|
||||
if chunk.sha == most_common:
|
||||
continue
|
||||
|
||||
if chunk.sha in frm_dict:
|
||||
sources['seed'].append(chunk.length)
|
||||
else:
|
||||
sources['remote_uncompressed'].append(chunk.length)
|
||||
sources['remote_compressed'].append(chunk_sizes[chunk.sha])
|
||||
|
||||
print()
|
||||
print("Update statistics (excluding zeros)")
|
||||
print()
|
||||
print("Download only with no seed:")
|
||||
print(f" Remote (uncompressed)\t\t{sum(sources['seed'] + sources['remote_uncompressed']) / 1000 / 1000:.2f} MB\tn = {len(to)}")
|
||||
print(f" Remote (compressed download)\t{sum(chunk_sizes.values()) / 1000 / 1000:.2f} MB\tn = {len(to)}")
|
||||
print()
|
||||
print("Upgrade with seed partition:")
|
||||
print(f" Seed (uncompressed)\t\t{sum(sources['seed']) / 1000 / 1000:.2f} MB\t\t\t\tn = {len(sources['seed'])}")
|
||||
sz, n = sum(sources['remote_uncompressed']), len(sources['remote_uncompressed'])
|
||||
print(f" Remote (uncompressed)\t\t{sz / 1000 / 1000:.2f} MB\t(avg {sz / 1000 / 1000 / n:4f} MB)\tn = {n}")
|
||||
sz, n = sum(sources['remote_compressed']), len(sources['remote_compressed'])
|
||||
print(f" Remote (compressed download)\t{sz / 1000 / 1000:.2f} MB\t(avg {sz / 1000 / 1000 / n:4f} MB)\tn = {n}")
|
||||
@@ -0,0 +1,20 @@
|
||||
import json
|
||||
import os
|
||||
import requests
|
||||
|
||||
TEST_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)))
|
||||
MANIFEST = os.path.join(TEST_DIR, "../agnos.json")
|
||||
|
||||
|
||||
class TestAgnosUpdater:
|
||||
|
||||
def test_manifest(self):
|
||||
with open(MANIFEST) as f:
|
||||
m = json.load(f)
|
||||
|
||||
for img in m:
|
||||
r = requests.head(img['url'], timeout=10)
|
||||
r.raise_for_status()
|
||||
assert r.headers['Content-Type'] == "application/x-xz"
|
||||
if not img['sparse']:
|
||||
assert img['hash'] == img['hash_raw']
|
||||
@@ -0,0 +1,70 @@
|
||||
import pytest
|
||||
import time
|
||||
import random
|
||||
import subprocess
|
||||
|
||||
from panda import Panda
|
||||
from openpilot.system.hardware import TICI, HARDWARE
|
||||
from openpilot.system.hardware.tici.hardware import Tici
|
||||
from openpilot.system.hardware.tici.amplifier import Amplifier
|
||||
|
||||
|
||||
class TestAmplifier:
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
if not TICI:
|
||||
pytest.skip()
|
||||
|
||||
def setup_method(self):
|
||||
# clear dmesg
|
||||
subprocess.check_call("sudo dmesg -C", shell=True)
|
||||
|
||||
HARDWARE.reset_internal_panda()
|
||||
Panda.wait_for_panda(None, 30)
|
||||
self.panda = Panda()
|
||||
|
||||
def teardown_method(self):
|
||||
HARDWARE.reset_internal_panda()
|
||||
|
||||
def _check_for_i2c_errors(self, expected):
|
||||
dmesg = subprocess.check_output("dmesg", shell=True, encoding='utf8')
|
||||
i2c_lines = [l for l in dmesg.strip().splitlines() if 'i2c_geni a88000.i2c' in l]
|
||||
i2c_str = '\n'.join(i2c_lines)
|
||||
|
||||
if not expected:
|
||||
return len(i2c_lines) == 0
|
||||
else:
|
||||
return "i2c error :-107" in i2c_str or "Bus arbitration lost" in i2c_str
|
||||
|
||||
def test_init(self):
|
||||
amp = Amplifier(debug=True)
|
||||
r = amp.initialize_configuration(Tici().get_device_type())
|
||||
assert r
|
||||
assert self._check_for_i2c_errors(False)
|
||||
|
||||
def test_shutdown(self):
|
||||
amp = Amplifier(debug=True)
|
||||
for _ in range(10):
|
||||
r = amp.set_global_shutdown(True)
|
||||
r = amp.set_global_shutdown(False)
|
||||
# amp config should be successful, with no i2c errors
|
||||
assert r
|
||||
assert self._check_for_i2c_errors(False)
|
||||
|
||||
def test_init_while_siren_play(self):
|
||||
for _ in range(10):
|
||||
self.panda.set_siren(False)
|
||||
time.sleep(0.1)
|
||||
|
||||
self.panda.set_siren(True)
|
||||
time.sleep(random.randint(0, 5))
|
||||
|
||||
amp = Amplifier(debug=True)
|
||||
r = amp.initialize_configuration(Tici().get_device_type())
|
||||
assert r
|
||||
|
||||
if self._check_for_i2c_errors(True):
|
||||
break
|
||||
else:
|
||||
pytest.fail("didn't hit any i2c errors")
|
||||
@@ -0,0 +1,51 @@
|
||||
import pytest
|
||||
|
||||
from openpilot.system.hardware import HARDWARE, TICI
|
||||
from openpilot.system.hardware.base import LPAProfileNotFoundError
|
||||
|
||||
# https://euicc-manual.osmocom.org/docs/rsp/known-test-profile
|
||||
# iccid is always the same for the given activation code
|
||||
TEST_ACTIVATION_CODE = 'LPA:1$rsp.truphone.com$QRF-BETTERROAMING-PMRDGIR2EARDEIT5'
|
||||
TEST_ICCID = '8944476500001944011'
|
||||
|
||||
TEST_NICKNAME = 'test_profile'
|
||||
|
||||
def cleanup():
|
||||
lpa = HARDWARE.get_sim_lpa()
|
||||
try:
|
||||
lpa.delete_profile(TEST_ICCID)
|
||||
except LPAProfileNotFoundError:
|
||||
pass
|
||||
lpa.process_notifications()
|
||||
|
||||
class TestEsim:
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
if not TICI:
|
||||
pytest.skip()
|
||||
cleanup()
|
||||
|
||||
@classmethod
|
||||
def teardown_class(cls):
|
||||
cleanup()
|
||||
|
||||
def test_provision_enable_disable(self):
|
||||
lpa = HARDWARE.get_sim_lpa()
|
||||
current_active = lpa.get_active_profile()
|
||||
|
||||
lpa.download_profile(TEST_ACTIVATION_CODE, TEST_NICKNAME)
|
||||
assert any(p.iccid == TEST_ICCID and p.nickname == TEST_NICKNAME for p in lpa.list_profiles())
|
||||
|
||||
lpa.enable_profile(TEST_ICCID)
|
||||
new_active = lpa.get_active_profile()
|
||||
assert new_active is not None
|
||||
assert new_active.iccid == TEST_ICCID
|
||||
assert new_active.nickname == TEST_NICKNAME
|
||||
|
||||
lpa.disable_profile(TEST_ICCID)
|
||||
new_active = lpa.get_active_profile()
|
||||
assert new_active is None
|
||||
|
||||
if current_active:
|
||||
lpa.enable_profile(current_active.iccid)
|
||||
@@ -0,0 +1,128 @@
|
||||
from collections import defaultdict, deque
|
||||
import pytest
|
||||
import time
|
||||
import numpy as np
|
||||
from dataclasses import dataclass
|
||||
from tabulate import tabulate
|
||||
|
||||
import cereal.messaging as messaging
|
||||
from cereal.services import SERVICE_LIST
|
||||
from opendbc.car.car_helpers import get_demo_car_params
|
||||
from openpilot.common.mock import mock_messages
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.hardware.tici.power_monitor import get_power
|
||||
from openpilot.system.manager.process_config import managed_processes
|
||||
from openpilot.system.manager.manager import manager_cleanup
|
||||
|
||||
SAMPLE_TIME = 8 # seconds to sample power
|
||||
MAX_WARMUP_TIME = 30 # seconds to wait for SAMPLE_TIME consecutive valid samples
|
||||
|
||||
@dataclass
|
||||
class Proc:
|
||||
procs: list[str]
|
||||
power: float
|
||||
msgs: list[str]
|
||||
rtol: float = 0.05
|
||||
atol: float = 0.12
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return '+'.join(self.procs)
|
||||
|
||||
|
||||
PROCS = [
|
||||
Proc(['camerad'], 1.65, atol=0.4, msgs=['roadCameraState', 'wideRoadCameraState', 'driverCameraState']),
|
||||
Proc(['modeld'], 1.24, atol=0.2, msgs=['modelV2']),
|
||||
Proc(['dmonitoringmodeld'], 0.65, atol=0.35, msgs=['driverStateV2']),
|
||||
Proc(['encoderd'], 0.23, msgs=[]),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.tici
|
||||
class TestPowerDraw:
|
||||
|
||||
def setup_method(self):
|
||||
Params().put("CarParams", get_demo_car_params().to_bytes())
|
||||
|
||||
# wait a bit for power save to disable
|
||||
time.sleep(5)
|
||||
|
||||
def teardown_method(self):
|
||||
manager_cleanup()
|
||||
|
||||
def get_expected_messages(self, proc):
|
||||
return int(sum(SAMPLE_TIME * SERVICE_LIST[msg].frequency for msg in proc.msgs))
|
||||
|
||||
def valid_msg_count(self, proc, msg_counts):
|
||||
msgs_received = sum(msg_counts[msg] for msg in proc.msgs)
|
||||
msgs_expected = self.get_expected_messages(proc)
|
||||
return np.isclose(msgs_expected, msgs_received, rtol=.02, atol=2)
|
||||
|
||||
def valid_power_draw(self, proc, used):
|
||||
return np.isclose(used, proc.power, rtol=proc.rtol, atol=proc.atol)
|
||||
|
||||
def tabulate_msg_counts(self, msgs_and_power):
|
||||
msg_counts = defaultdict(int)
|
||||
for _, counts in msgs_and_power:
|
||||
for msg, count in counts.items():
|
||||
msg_counts[msg] += count
|
||||
return msg_counts
|
||||
|
||||
def get_power_with_warmup_for_target(self, proc, prev):
|
||||
socks = {msg: messaging.sub_sock(msg) for msg in proc.msgs}
|
||||
for sock in socks.values():
|
||||
messaging.drain_sock_raw(sock)
|
||||
|
||||
msgs_and_power = deque([], maxlen=SAMPLE_TIME)
|
||||
|
||||
start_time = time.monotonic()
|
||||
|
||||
while (time.monotonic() - start_time) < MAX_WARMUP_TIME:
|
||||
power = get_power(1)
|
||||
iteration_msg_counts = {}
|
||||
for msg,sock in socks.items():
|
||||
iteration_msg_counts[msg] = len(messaging.drain_sock_raw(sock))
|
||||
msgs_and_power.append((power, iteration_msg_counts))
|
||||
|
||||
if len(msgs_and_power) < SAMPLE_TIME:
|
||||
continue
|
||||
|
||||
msg_counts = self.tabulate_msg_counts(msgs_and_power)
|
||||
now = np.mean([m[0] for m in msgs_and_power])
|
||||
|
||||
if self.valid_msg_count(proc, msg_counts) and self.valid_power_draw(proc, now - prev):
|
||||
break
|
||||
|
||||
return now, msg_counts, time.monotonic() - start_time - SAMPLE_TIME
|
||||
|
||||
@mock_messages(['livePose'])
|
||||
def test_camera_procs(self, subtests):
|
||||
baseline = get_power()
|
||||
|
||||
prev = baseline
|
||||
used = {}
|
||||
warmup_time = {}
|
||||
msg_counts = {}
|
||||
|
||||
for proc in PROCS:
|
||||
for p in proc.procs:
|
||||
managed_processes[p].start()
|
||||
now, local_msg_counts, warmup_time[proc.name] = self.get_power_with_warmup_for_target(proc, prev)
|
||||
msg_counts.update(local_msg_counts)
|
||||
|
||||
used[proc.name] = now - prev
|
||||
prev = now
|
||||
|
||||
manager_cleanup()
|
||||
|
||||
tab = [['process', 'expected (W)', 'measured (W)', '# msgs expected', '# msgs received', "warmup time (s)"]]
|
||||
for proc in PROCS:
|
||||
cur = used[proc.name]
|
||||
expected = proc.power
|
||||
msgs_received = sum(msg_counts[msg] for msg in proc.msgs)
|
||||
tab.append([proc.name, round(expected, 2), round(cur, 2), self.get_expected_messages(proc), msgs_received, round(warmup_time[proc.name], 2)])
|
||||
with subtests.test(proc=proc.name):
|
||||
assert self.valid_msg_count(proc, msg_counts), f"expected {self.get_expected_messages(proc)} msgs, got {msgs_received} msgs"
|
||||
assert self.valid_power_draw(proc, cur), f"expected {expected:.2f}W, got {cur:.2f}W"
|
||||
print(tabulate(tab))
|
||||
print(f"Baseline {baseline:.2f}W\n")
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
|
||||
|
||||
AGNOS_PY=$1
|
||||
MANIFEST=$2
|
||||
|
||||
if [[ ! -f "$AGNOS_PY" || ! -f "$MANIFEST" ]]; then
|
||||
echo "invalid args"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if systemctl is-active --quiet weston-ready; then
|
||||
$DIR/updater_weston $AGNOS_PY $MANIFEST
|
||||
else
|
||||
$DIR/updater_magic $AGNOS_PY $MANIFEST
|
||||
fi
|
||||
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Reference in New Issue
Block a user