This commit is contained in:
Your Name
2024-04-27 13:03:43 -05:00
parent fe5f722eda
commit 71b83ffe7f
22 changed files with 986 additions and 28 deletions

1
system/hardware/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
eon/rat

42
system/hardware/base.h Normal file
View File

@@ -0,0 +1,42 @@
#pragma once
#include <cstdlib>
#include <fstream>
#include <map>
#include <string>
#include "cereal/messaging/messaging.h"
// no-op base hw class
class HardwareNone {
public:
static constexpr float MAX_VOLUME = 0.7;
static constexpr float MIN_VOLUME = 0.2;
static std::string get_os_version() { return ""; }
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 reboot() {}
static void soft_reboot() {}
static void poweroff() {}
static void set_brightness(int percent) {}
static void set_display_power(bool on) {}
static bool get_ssh_enabled() { return false; }
static void set_ssh_enabled(bool enabled) {}
static void config_cpu_rendering(bool offscreen);
static bool PC() { return false; }
static bool TICI() { return false; }
static bool AGNOS() { return false; }
};

View File

@@ -1,6 +1,5 @@
from abc import abstractmethod, ABC from abc import abstractmethod, ABC
from collections import namedtuple from collections import namedtuple
from typing import Dict
from cereal import log from cereal import log
@@ -10,7 +9,7 @@ NetworkType = log.DeviceState.NetworkType
class HardwareBase(ABC): class HardwareBase(ABC):
@staticmethod @staticmethod
def get_cmdline() -> Dict[str, str]: def get_cmdline() -> dict[str, str]:
with open('/proc/cmdline') as f: with open('/proc/cmdline') as f:
cmdline = f.read() cmdline = f.read()
return {kv[0]: kv[1] for kv in [s.split('=') for s in cmdline.split(' ')] if len(kv) == 2} return {kv[0]: kv[1] for kv in [s.split('=') for s in cmdline.split(' ')] if len(kv) == 2}
@@ -30,6 +29,10 @@ class HardwareBase(ABC):
def reboot(self, reason=None): def reboot(self, reason=None):
pass pass
@abstractmethod
def soft_reboot(self):
pass
@abstractmethod @abstractmethod
def uninstall(self): def uninstall(self):
pass pass

50
system/hardware/hw.h Normal file
View File

@@ -0,0 +1,50 @@
#pragma once
#include <string>
#include "system/hardware/base.h"
#include "common/util.h"
#if QCOM2
#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() + "/";
}
} // namespace Path

View File

@@ -0,0 +1,23 @@
#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; }
static void config_cpu_rendering(bool offscreen) {
if (offscreen) {
setenv("QT_QPA_PLATFORM", "offscreen", 1);
}
setenv("__GLX_VENDOR_LIBRARY_NAME", "mesa", 1);
setenv("LP_NUM_THREADS", "0", 1); // disable threading so we stay on our assigned CPU
}
};

View File

@@ -20,6 +20,9 @@ class Pc(HardwareBase):
def reboot(self, reason=None): def reboot(self, reason=None):
print("REBOOT!") print("REBOOT!")
def soft_reboot(self):
print("SOFT REBOOT!")
def uninstall(self): def uninstall(self):
print("uninstall") print("uninstall")

View File

@@ -6,7 +6,7 @@ import os
import struct import struct
import subprocess import subprocess
import time import time
from typing import Dict, Generator, List, Tuple, Union from collections.abc import Generator
import requests import requests
@@ -117,7 +117,7 @@ def get_raw_hash(path: str, partition_size: int) -> str:
return raw_hash.hexdigest().lower() return raw_hash.hexdigest().lower()
def verify_partition(target_slot_number: int, partition: Dict[str, Union[str, int]], force_full_check: bool = False) -> bool: 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 full_check = partition['full_check'] or force_full_check
path = get_partition_path(target_slot_number, partition) path = get_partition_path(target_slot_number, partition)
@@ -184,7 +184,7 @@ def extract_casync_image(target_slot_number: int, partition: dict, cloudlog):
target = casync.parse_caibx(partition['casync_caibx']) target = casync.parse_caibx(partition['casync_caibx'])
sources: List[Tuple[str, casync.ChunkReader, casync.ChunkDict]] = [] sources: list[tuple[str, casync.ChunkReader, casync.ChunkDict]] = []
# First source is the current partition. # First source is the current partition.
try: try:

View File

@@ -2,7 +2,6 @@
import time import time
from smbus2 import SMBus from smbus2 import SMBus
from collections import namedtuple from collections import namedtuple
from typing import List
# https://datasheets.maximintegrated.com/en/ds/MAX98089.pdf # https://datasheets.maximintegrated.com/en/ds/MAX98089.pdf
@@ -110,7 +109,7 @@ class Amplifier:
def _get_shutdown_config(self, amp_disabled: bool) -> AmpConfig: def _get_shutdown_config(self, amp_disabled: bool) -> AmpConfig:
return AmpConfig("Global shutdown", 0b0 if amp_disabled else 0b1, 0x51, 7, 0b10000000) return AmpConfig("Global shutdown", 0b0 if amp_disabled else 0b1, 0x51, 7, 0b10000000)
def _set_configs(self, configs: List[AmpConfig]) -> None: def _set_configs(self, configs: list[AmpConfig]) -> None:
with SMBus(self.AMP_I2C_BUS) as bus: with SMBus(self.AMP_I2C_BUS) as bus:
for config in configs: for config in configs:
if self.debug: if self.debug:
@@ -123,7 +122,7 @@ class Amplifier:
if self.debug: if self.debug:
print(f" Changed {hex(config.register)}: {hex(old_value)} -> {hex(new_value)}") print(f" Changed {hex(config.register)}: {hex(old_value)} -> {hex(new_value)}")
def set_configs(self, configs: List[AmpConfig]) -> bool: def set_configs(self, configs: list[AmpConfig]) -> bool:
# retry in case panda is using the amp # retry in case panda is using the amp
tries = 15 tries = 15
for i in range(15): for i in range(15):

View File

@@ -7,7 +7,7 @@ import sys
import time import time
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections import defaultdict, namedtuple from collections import defaultdict, namedtuple
from typing import Callable, Dict, List, Optional, Tuple from collections.abc import Callable
import requests import requests
from Crypto.Hash import SHA512 from Crypto.Hash import SHA512
@@ -28,7 +28,7 @@ CHUNK_DOWNLOAD_RETRIES = 3
CAIBX_DOWNLOAD_TIMEOUT = 120 CAIBX_DOWNLOAD_TIMEOUT = 120
Chunk = namedtuple('Chunk', ['sha', 'offset', 'length']) Chunk = namedtuple('Chunk', ['sha', 'offset', 'length'])
ChunkDict = Dict[bytes, Chunk] ChunkDict = dict[bytes, Chunk]
class ChunkReader(ABC): class ChunkReader(ABC):
@@ -83,7 +83,7 @@ class RemoteChunkReader(ChunkReader):
return decompressor.decompress(contents) return decompressor.decompress(contents)
def parse_caibx(caibx_path: str) -> List[Chunk]: def parse_caibx(caibx_path: str) -> list[Chunk]:
"""Parses the chunks from a caibx file. Can handle both local and remote files. """Parses the chunks from a caibx file. Can handle both local and remote files.
Returns a list of chunks with hash, offset and length""" Returns a list of chunks with hash, offset and length"""
caibx: io.BufferedIOBase caibx: io.BufferedIOBase
@@ -132,7 +132,7 @@ def parse_caibx(caibx_path: str) -> List[Chunk]:
return chunks return chunks
def build_chunk_dict(chunks: List[Chunk]) -> ChunkDict: def build_chunk_dict(chunks: list[Chunk]) -> ChunkDict:
"""Turn a list of chunks into a dict for faster lookups based on hash. """Turn a list of chunks into a dict for faster lookups based on hash.
Keep first chunk since it's more likely to be already downloaded.""" Keep first chunk since it's more likely to be already downloaded."""
r = {} r = {}
@@ -142,11 +142,11 @@ def build_chunk_dict(chunks: List[Chunk]) -> ChunkDict:
return r return r
def extract(target: List[Chunk], def extract(target: list[Chunk],
sources: List[Tuple[str, ChunkReader, ChunkDict]], sources: list[tuple[str, ChunkReader, ChunkDict]],
out_path: str, out_path: str,
progress: Optional[Callable[[int], None]] = None): progress: Callable[[int], None] = None):
stats: Dict[str, int] = defaultdict(int) stats: dict[str, int] = defaultdict(int)
mode = 'rb+' if os.path.exists(out_path) else 'wb' mode = 'rb+' if os.path.exists(out_path) else 'wb'
with open(out_path, mode) as out: with open(out_path, mode) as out:
@@ -181,7 +181,7 @@ def extract(target: List[Chunk],
return stats return stats
def print_stats(stats: Dict[str, int]): def print_stats(stats: dict[str, int]):
total_bytes = sum(stats.values()) total_bytes = sum(stats.values())
print(f"Total size: {total_bytes / 1024 / 1024:.2f} MB") print(f"Total size: {total_bytes / 1024 / 1024:.2f} MB")
for name, total in stats.items(): for name, total in stats.items():

View File

@@ -0,0 +1,115 @@
#!/usr/bin/env python3
import os
import math
import time
import binascii
import requests
import serial
import subprocess
def post(url, payload):
print()
print("POST to", url)
r = requests.post(
url,
data=payload,
verify=False,
headers={
"Content-Type": "application/json",
"X-Admin-Protocol": "gsma/rsp/v2.2.0",
"charset": "utf-8",
"User-Agent": "gsma-rsp-lpad",
},
)
print("resp", r)
print("resp text", repr(r.text))
print()
r.raise_for_status()
ret = f"HTTP/1.1 {r.status_code}"
ret += ''.join(f"{k}: {v}" for k, v in r.headers.items() if k != 'Connection')
return ret.encode() + r.content
class LPA:
def __init__(self):
self.dev = serial.Serial('/dev/ttyUSB2', baudrate=57600, timeout=1, bytesize=8)
self.dev.reset_input_buffer()
self.dev.reset_output_buffer()
assert "OK" in self.at("AT")
def at(self, cmd):
print(f"==> {cmd}")
self.dev.write(cmd.encode() + b'\r\n')
r = b""
cnt = 0
while b"OK" not in r and b"ERROR" not in r and cnt < 20:
r += self.dev.read(8192).strip()
cnt += 1
r = r.decode()
print(f"<== {repr(r)}")
return r
def download_ota(self, qr):
return self.at(f'AT+QESIM="ota","{qr}"')
def download(self, qr):
smdp = qr.split('$')[1]
out = self.at(f'AT+QESIM="download","{qr}"')
for _ in range(5):
line = out.split("+QESIM: ")[1].split("\r\n\r\nOK")[0]
parts = [x.strip().strip('"') for x in line.split(',', maxsplit=4)]
print(repr(parts))
trans, ret, url, payloadlen, payload = parts
assert trans == "trans" and ret == "0"
assert len(payload) == int(payloadlen)
r = post(f"https://{smdp}/{url}", payload)
to_send = binascii.hexlify(r).decode()
chunk_len = 1400
for i in range(math.ceil(len(to_send) / chunk_len)):
state = 1 if (i+1)*chunk_len < len(to_send) else 0
data = to_send[i * chunk_len : (i+1)*chunk_len]
out = self.at(f'AT+QESIM="trans",{len(to_send)},{state},{i},{len(data)},"{data}"')
assert "OK" in out
if '+QESIM:"download",1' in out:
raise Exception("profile install failed")
elif '+QESIM:"download",0' in out:
print("done, successfully loaded")
break
def enable(self, iccid):
self.at(f'AT+QESIM="enable","{iccid}"')
def disable(self, iccid):
self.at(f'AT+QESIM="disable","{iccid}"')
def delete(self, iccid):
self.at(f'AT+QESIM="delete","{iccid}"')
def list_profiles(self):
out = self.at('AT+QESIM="list"')
return out.strip().splitlines()[1:]
if __name__ == "__main__":
import sys
if "RESTART" in os.environ:
subprocess.check_call("sudo systemctl stop ModemManager", shell=True)
subprocess.check_call("/usr/comma/lte/lte.sh stop_blocking", shell=True)
subprocess.check_call("/usr/comma/lte/lte.sh start", shell=True)
while not os.path.exists('/dev/ttyUSB2'):
time.sleep(1)
time.sleep(3)
lpa = LPA()
print(lpa.list_profiles())
if len(sys.argv) > 1:
lpa.download(sys.argv[1])
print(lpa.list_profiles())

View File

@@ -0,0 +1,123 @@
#pragma once
#include <cstdlib>
#include <fstream>
#include <map>
#include <string>
#include "common/params.h"
#include "common/util.h"
#include "system/hardware/base.h"
class HardwareTici : public HardwareNone {
public:
static constexpr float MAX_VOLUME = 0.9;
static constexpr float MIN_VOLUME = 0.1;
static bool TICI() { return true; }
static bool AGNOS() { return true; }
static std::string get_os_version() {
return "AGNOS " + util::read_file("/VERSION");
}
static std::string get_name() {
std::string model = util::read_file("/sys/firmware/devicetree/base/model");
return model.substr(std::string("comma ").size());
}
static cereal::InitData::DeviceType get_device_type() {
return (get_name() == "tizi") ? cereal::InitData::DeviceType::TIZI : (get_name() == "mici" ? cereal::InitData::DeviceType::MICI : cereal::InitData::DeviceType::TICI);
}
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 reboot() { std::system("sudo reboot"); }
static void soft_reboot() {
const std::vector<std::string> commands = {
"rm -f /tmp/safe_staging_overlay.lock",
"tmux new -s commatmp -d '/data/continue.sh'",
"tmux kill-session -t comma",
"tmux rename comma"
};
for (const auto& cmd : commands) {
int result;
do {
result = std::system(cmd.c_str());
} while (result != 0);
if (result != 0) {
reboot();
}
}
}
static void poweroff() { std::system("sudo poweroff"); }
static void set_brightness(int percent) {
std::string max = util::read_file("/sys/class/backlight/panel0-backlight/max_brightness");
std::ofstream brightness_control("/sys/class/backlight/panel0-backlight/brightness");
if (brightness_control.is_open()) {
brightness_control << (int)(percent * (std::stof(max)/100.)) << "\n";
brightness_control.close();
}
}
static void set_display_power(bool on) {
std::ofstream bl_power_control("/sys/class/backlight/panel0-backlight/bl_power");
if (bl_power_control.is_open()) {
bl_power_control << (on ? "0" : "4") << "\n";
bl_power_control.close();
}
}
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;
}
static bool get_ssh_enabled() { return Params().getBool("SshEnabled"); }
static void set_ssh_enabled(bool enabled) { Params().putBool("SshEnabled", enabled); }
static void config_cpu_rendering(bool offscreen) {
if (offscreen) {
setenv("QT_QPA_PLATFORM", "eglfs", 1); // offscreen doesn't work with EGL/GLES
}
setenv("LP_NUM_THREADS", "0", 1); // disable threading so we stay on our assigned CPU
}
};

View File

@@ -94,11 +94,7 @@ def get_device_type():
# lru_cache and cache can cause memory leaks when used in classes # lru_cache and cache can cause memory leaks when used in classes
with open("/sys/firmware/devicetree/base/model") as f: with open("/sys/firmware/devicetree/base/model") as f:
model = f.read().strip('\x00') model = f.read().strip('\x00')
model = model.split('comma ')[-1] return model.split('comma ')[-1]
# TODO: remove this with AGNOS 7+
if model.startswith('Qualcomm'):
model = 'tici'
return model
class Tici(HardwareBase): class Tici(HardwareBase):
@cached_property @cached_property
@@ -116,6 +112,8 @@ class Tici(HardwareBase):
@cached_property @cached_property
def amplifier(self): def amplifier(self):
if self.get_device_type() == "mici":
return None
return Amplifier() return Amplifier()
def get_os_version(self): def get_os_version(self):
@@ -134,6 +132,16 @@ class Tici(HardwareBase):
def reboot(self, reason=None): def reboot(self, reason=None):
subprocess.check_output(["sudo", "reboot"]) subprocess.check_output(["sudo", "reboot"])
def soft_reboot(self):
commands = [
['rm', '-f', '/tmp/safe_staging_overlay.lock'],
['tmux', 'new', '-s', 'commatmp', '-d', '/data/continue.sh'],
['tmux', 'kill-session', '-t', 'comma'],
['tmux', 'rename', 'comma'],
]
for command in commands:
subprocess.run(command, stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL)
def uninstall(self): def uninstall(self):
Path("/data/__system_reset__").touch() Path("/data/__system_reset__").touch()
os.sync() os.sync()
@@ -374,9 +382,10 @@ class Tici(HardwareBase):
def set_power_save(self, powersave_enabled): def set_power_save(self, powersave_enabled):
# amplifier, 100mW at idle # amplifier, 100mW at idle
self.amplifier.set_global_shutdown(amp_disabled=powersave_enabled) if self.amplifier is not None:
if not powersave_enabled: self.amplifier.set_global_shutdown(amp_disabled=powersave_enabled)
self.amplifier.initialize_configuration(self.get_device_type()) if not powersave_enabled:
self.amplifier.initialize_configuration(self.get_device_type())
# *** CPU config *** # *** CPU config ***
@@ -414,7 +423,8 @@ class Tici(HardwareBase):
return 0 return 0
def initialize_hardware(self): def initialize_hardware(self):
self.amplifier.initialize_configuration(self.get_device_type()) if self.amplifier is not None:
self.amplifier.initialize_configuration(self.get_device_type())
# Allow thermald to write engagement status to kmsg # Allow thermald to write engagement status to kmsg
os.system("sudo chmod a+w /dev/kmsg") os.system("sudo chmod a+w /dev/kmsg")
@@ -468,8 +478,9 @@ class Tici(HardwareBase):
# use sim slot # use sim slot
'AT^SIMSWAP=1', 'AT^SIMSWAP=1',
# configure ECM mode # ethernet config
'AT$QCPCFG=usbNet,1' 'AT$QCPCFG=usbNet,0',
'AT$QCNETDEVCTL=3,1',
] ]
else: else:
cmds += [ cmds += [
@@ -478,6 +489,12 @@ class Tici(HardwareBase):
'AT+QNVFW="/nv/item_files/ims/IMS_enable",00', 'AT+QNVFW="/nv/item_files/ims/IMS_enable",00',
'AT+QNVFW="/nv/item_files/modem/mmode/ue_usage_setting",01', 'AT+QNVFW="/nv/item_files/modem/mmode/ue_usage_setting",01',
] ]
if self.get_device_type() == "tizi":
cmds += [
# SIM hot swap
'AT+QSIMDET=1,0',
'AT+QSIMSTAT=1',
]
# clear out old blue prime initial APN # clear out old blue prime initial APN
os.system('mmcli -m any --3gpp-set-initial-eps-bearer-settings="apn="') os.system('mmcli -m any --3gpp-set-initial-eps-bearer-settings="apn="')

View File

@@ -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}")

View File

@@ -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}")

View File

@@ -0,0 +1,18 @@
#!/usr/bin/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

View File

View File

@@ -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}")

View File

@@ -0,0 +1,26 @@
#!/usr/bin/env python3
import json
import os
import unittest
import requests
TEST_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)))
MANIFEST = os.path.join(TEST_DIR, "../agnos.json")
class TestAgnosUpdater(unittest.TestCase):
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()
self.assertEqual(r.headers['Content-Type'], "application/x-xz")
if not img['sparse']:
assert img['hash'] == img['hash_raw']
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,75 @@
#!/usr/bin/env python3
import time
import random
import unittest
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(unittest.TestCase):
@classmethod
def setUpClass(cls):
if not TICI:
raise unittest.SkipTest
def setUp(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(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:
self.fail("didn't hit any i2c errors")
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,153 @@
#!/usr/bin/env python3
import os
import unittest
import tempfile
import subprocess
import openpilot.system.hardware.tici.casync as casync
# dd if=/dev/zero of=/tmp/img.raw bs=1M count=2
# sudo losetup -f /tmp/img.raw
# losetup -a | grep img.raw
LOOPBACK = os.environ.get('LOOPBACK', None)
class TestCasync(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.tmpdir = tempfile.TemporaryDirectory()
# Build example contents
chunk_a = [i % 256 for i in range(1024)] * 512
chunk_b = [(256 - i) % 256 for i in range(1024)] * 512
zeroes = [0] * (1024 * 128)
contents = chunk_a + chunk_b + zeroes + chunk_a
cls.contents = bytes(contents)
# Write to file
cls.orig_fn = os.path.join(cls.tmpdir.name, 'orig.bin')
with open(cls.orig_fn, 'wb') as f:
f.write(cls.contents)
# Create casync files
cls.manifest_fn = os.path.join(cls.tmpdir.name, 'orig.caibx')
cls.store_fn = os.path.join(cls.tmpdir.name, 'store')
subprocess.check_output(["casync", "make", "--compression=xz", "--store", cls.store_fn, cls.manifest_fn, cls.orig_fn])
target = casync.parse_caibx(cls.manifest_fn)
hashes = [c.sha.hex() for c in target]
# Ensure we have chunk reuse
assert len(hashes) > len(set(hashes))
def setUp(self):
# Clear target_lo
if LOOPBACK is not None:
self.target_lo = LOOPBACK
with open(self.target_lo, 'wb') as f:
f.write(b"0" * len(self.contents))
self.target_fn = os.path.join(self.tmpdir.name, next(tempfile._get_candidate_names()))
self.seed_fn = os.path.join(self.tmpdir.name, next(tempfile._get_candidate_names()))
def tearDown(self):
for fn in [self.target_fn, self.seed_fn]:
try:
os.unlink(fn)
except FileNotFoundError:
pass
def test_simple_extract(self):
target = casync.parse_caibx(self.manifest_fn)
sources = [('remote', casync.RemoteChunkReader(self.store_fn), casync.build_chunk_dict(target))]
stats = casync.extract(target, sources, self.target_fn)
with open(self.target_fn, 'rb') as target_f:
self.assertEqual(target_f.read(), self.contents)
self.assertEqual(stats['remote'], len(self.contents))
def test_seed(self):
target = casync.parse_caibx(self.manifest_fn)
# Populate seed with half of the target contents
with open(self.seed_fn, 'wb') as seed_f:
seed_f.write(self.contents[:len(self.contents) // 2])
sources = [('seed', casync.FileChunkReader(self.seed_fn), casync.build_chunk_dict(target))]
sources += [('remote', casync.RemoteChunkReader(self.store_fn), casync.build_chunk_dict(target))]
stats = casync.extract(target, sources, self.target_fn)
with open(self.target_fn, 'rb') as target_f:
self.assertEqual(target_f.read(), self.contents)
self.assertGreater(stats['seed'], 0)
self.assertLess(stats['remote'], len(self.contents))
def test_already_done(self):
"""Test that an already flashed target doesn't download any chunks"""
target = casync.parse_caibx(self.manifest_fn)
with open(self.target_fn, 'wb') as f:
f.write(self.contents)
sources = [('target', casync.FileChunkReader(self.target_fn), casync.build_chunk_dict(target))]
sources += [('remote', casync.RemoteChunkReader(self.store_fn), casync.build_chunk_dict(target))]
stats = casync.extract(target, sources, self.target_fn)
with open(self.target_fn, 'rb') as f:
self.assertEqual(f.read(), self.contents)
self.assertEqual(stats['target'], len(self.contents))
def test_chunk_reuse(self):
"""Test that chunks that are reused are only downloaded once"""
target = casync.parse_caibx(self.manifest_fn)
# Ensure target exists
with open(self.target_fn, 'wb'):
pass
sources = [('target', casync.FileChunkReader(self.target_fn), casync.build_chunk_dict(target))]
sources += [('remote', casync.RemoteChunkReader(self.store_fn), casync.build_chunk_dict(target))]
stats = casync.extract(target, sources, self.target_fn)
with open(self.target_fn, 'rb') as f:
self.assertEqual(f.read(), self.contents)
self.assertLess(stats['remote'], len(self.contents))
@unittest.skipUnless(LOOPBACK, "requires loopback device")
def test_lo_simple_extract(self):
target = casync.parse_caibx(self.manifest_fn)
sources = [('remote', casync.RemoteChunkReader(self.store_fn), casync.build_chunk_dict(target))]
stats = casync.extract(target, sources, self.target_lo)
with open(self.target_lo, 'rb') as target_f:
self.assertEqual(target_f.read(len(self.contents)), self.contents)
self.assertEqual(stats['remote'], len(self.contents))
@unittest.skipUnless(LOOPBACK, "requires loopback device")
def test_lo_chunk_reuse(self):
"""Test that chunks that are reused are only downloaded once"""
target = casync.parse_caibx(self.manifest_fn)
sources = [('target', casync.FileChunkReader(self.target_lo), casync.build_chunk_dict(target))]
sources += [('remote', casync.RemoteChunkReader(self.store_fn), casync.build_chunk_dict(target))]
stats = casync.extract(target, sources, self.target_lo)
with open(self.target_lo, 'rb') as f:
self.assertEqual(f.read(len(self.contents)), self.contents)
self.assertLess(stats['remote'], len(self.contents))
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,28 @@
#!/usr/bin/env python3
import pytest
import time
import unittest
import numpy as np
from openpilot.system.hardware.tici.hardware import Tici
HARDWARE = Tici()
@pytest.mark.tici
class TestHardware(unittest.TestCase):
def test_power_save_time(self):
ts = []
for _ in range(5):
for on in (True, False):
st = time.monotonic()
HARDWARE.set_power_save(on)
ts.append(time.monotonic() - st)
assert 0.1 < np.mean(ts) < 0.25
assert max(ts) < 0.3
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,134 @@
#!/usr/bin/env python3
from collections import defaultdict, deque
import pytest
import unittest
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 openpilot.common.mock import mock_messages
from openpilot.selfdrive.car.car_helpers import write_car_param
from openpilot.system.hardware.tici.power_monitor import get_power
from openpilot.selfdrive.manager.process_config import managed_processes
from openpilot.selfdrive.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'], 2.1, msgs=['roadCameraState', 'wideRoadCameraState', 'driverCameraState']),
Proc(['modeld'], 1.12, atol=0.2, msgs=['modelV2']),
Proc(['dmonitoringmodeld'], 0.4, msgs=['driverStateV2']),
Proc(['encoderd'], 0.23, msgs=[]),
Proc(['mapsd', 'navmodeld'], 0.05, msgs=['mapRenderState', 'navModel']),
]
@pytest.mark.tici
class TestPowerDraw(unittest.TestCase):
def setUp(self):
write_car_param()
# wait a bit for power save to disable
time.sleep(5)
def tearDown(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.core.numeric.isclose(msgs_expected, msgs_received, rtol=.02, atol=2)
def valid_power_draw(self, proc, used):
return np.core.numeric.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(['liveLocationKalman'])
def test_camera_procs(self):
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 self.subTest(proc=proc.name):
self.assertTrue(self.valid_msg_count(proc, msg_counts), f"expected {self.get_expected_messages(proc)} msgs, got {msgs_received} msgs")
self.assertTrue(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")
if __name__ == "__main__":
unittest.main()