wip
This commit is contained in:
1
system/hardware/.gitignore
vendored
Normal file
1
system/hardware/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
eon/rat
|
||||
42
system/hardware/base.h
Normal file
42
system/hardware/base.h
Normal 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; }
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
from abc import abstractmethod, ABC
|
||||
from collections import namedtuple
|
||||
from typing import Dict
|
||||
|
||||
from cereal import log
|
||||
|
||||
@@ -10,7 +9,7 @@ NetworkType = log.DeviceState.NetworkType
|
||||
|
||||
class HardwareBase(ABC):
|
||||
@staticmethod
|
||||
def get_cmdline() -> Dict[str, str]:
|
||||
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}
|
||||
@@ -30,6 +29,10 @@ class HardwareBase(ABC):
|
||||
def reboot(self, reason=None):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def soft_reboot(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def uninstall(self):
|
||||
pass
|
||||
|
||||
50
system/hardware/hw.h
Normal file
50
system/hardware/hw.h
Normal 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
|
||||
23
system/hardware/pc/hardware.h
Normal file
23
system/hardware/pc/hardware.h
Normal 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
|
||||
}
|
||||
};
|
||||
@@ -20,6 +20,9 @@ class Pc(HardwareBase):
|
||||
def reboot(self, reason=None):
|
||||
print("REBOOT!")
|
||||
|
||||
def soft_reboot(self):
|
||||
print("SOFT REBOOT!")
|
||||
|
||||
def uninstall(self):
|
||||
print("uninstall")
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import os
|
||||
import struct
|
||||
import subprocess
|
||||
import time
|
||||
from typing import Dict, Generator, List, Tuple, Union
|
||||
from collections.abc import Generator
|
||||
|
||||
import requests
|
||||
|
||||
@@ -117,7 +117,7 @@ def get_raw_hash(path: str, partition_size: int) -> str:
|
||||
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
|
||||
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'])
|
||||
|
||||
sources: List[Tuple[str, casync.ChunkReader, casync.ChunkDict]] = []
|
||||
sources: list[tuple[str, casync.ChunkReader, casync.ChunkDict]] = []
|
||||
|
||||
# First source is the current partition.
|
||||
try:
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import time
|
||||
from smbus2 import SMBus
|
||||
from collections import namedtuple
|
||||
from typing import List
|
||||
|
||||
# https://datasheets.maximintegrated.com/en/ds/MAX98089.pdf
|
||||
|
||||
@@ -110,7 +109,7 @@ class Amplifier:
|
||||
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:
|
||||
def _set_configs(self, configs: list[AmpConfig]) -> None:
|
||||
with SMBus(self.AMP_I2C_BUS) as bus:
|
||||
for config in configs:
|
||||
if self.debug:
|
||||
@@ -123,7 +122,7 @@ class Amplifier:
|
||||
if self.debug:
|
||||
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
|
||||
tries = 15
|
||||
for i in range(15):
|
||||
|
||||
@@ -7,7 +7,7 @@ import sys
|
||||
import time
|
||||
from abc import ABC, abstractmethod
|
||||
from collections import defaultdict, namedtuple
|
||||
from typing import Callable, Dict, List, Optional, Tuple
|
||||
from collections.abc import Callable
|
||||
|
||||
import requests
|
||||
from Crypto.Hash import SHA512
|
||||
@@ -28,7 +28,7 @@ CHUNK_DOWNLOAD_RETRIES = 3
|
||||
CAIBX_DOWNLOAD_TIMEOUT = 120
|
||||
|
||||
Chunk = namedtuple('Chunk', ['sha', 'offset', 'length'])
|
||||
ChunkDict = Dict[bytes, Chunk]
|
||||
ChunkDict = dict[bytes, Chunk]
|
||||
|
||||
|
||||
class ChunkReader(ABC):
|
||||
@@ -83,7 +83,7 @@ class RemoteChunkReader(ChunkReader):
|
||||
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.
|
||||
Returns a list of chunks with hash, offset and length"""
|
||||
caibx: io.BufferedIOBase
|
||||
@@ -132,7 +132,7 @@ def parse_caibx(caibx_path: str) -> List[Chunk]:
|
||||
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.
|
||||
Keep first chunk since it's more likely to be already downloaded."""
|
||||
r = {}
|
||||
@@ -142,11 +142,11 @@ def build_chunk_dict(chunks: List[Chunk]) -> ChunkDict:
|
||||
return r
|
||||
|
||||
|
||||
def extract(target: List[Chunk],
|
||||
sources: List[Tuple[str, ChunkReader, ChunkDict]],
|
||||
def extract(target: list[Chunk],
|
||||
sources: list[tuple[str, ChunkReader, ChunkDict]],
|
||||
out_path: str,
|
||||
progress: Optional[Callable[[int], None]] = None):
|
||||
stats: Dict[str, int] = defaultdict(int)
|
||||
progress: Callable[[int], None] = None):
|
||||
stats: dict[str, int] = defaultdict(int)
|
||||
|
||||
mode = 'rb+' if os.path.exists(out_path) else 'wb'
|
||||
with open(out_path, mode) as out:
|
||||
@@ -181,7 +181,7 @@ def extract(target: List[Chunk],
|
||||
return stats
|
||||
|
||||
|
||||
def print_stats(stats: Dict[str, int]):
|
||||
def print_stats(stats: dict[str, int]):
|
||||
total_bytes = sum(stats.values())
|
||||
print(f"Total size: {total_bytes / 1024 / 1024:.2f} MB")
|
||||
for name, total in stats.items():
|
||||
|
||||
115
system/hardware/tici/esim.py
Normal file
115
system/hardware/tici/esim.py
Normal 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())
|
||||
123
system/hardware/tici/hardware.h
Normal file
123
system/hardware/tici/hardware.h
Normal 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
|
||||
}
|
||||
};
|
||||
@@ -94,11 +94,7 @@ 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')
|
||||
model = model.split('comma ')[-1]
|
||||
# TODO: remove this with AGNOS 7+
|
||||
if model.startswith('Qualcomm'):
|
||||
model = 'tici'
|
||||
return model
|
||||
return model.split('comma ')[-1]
|
||||
|
||||
class Tici(HardwareBase):
|
||||
@cached_property
|
||||
@@ -116,6 +112,8 @@ class Tici(HardwareBase):
|
||||
|
||||
@cached_property
|
||||
def amplifier(self):
|
||||
if self.get_device_type() == "mici":
|
||||
return None
|
||||
return Amplifier()
|
||||
|
||||
def get_os_version(self):
|
||||
@@ -134,6 +132,16 @@ class Tici(HardwareBase):
|
||||
def reboot(self, reason=None):
|
||||
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):
|
||||
Path("/data/__system_reset__").touch()
|
||||
os.sync()
|
||||
@@ -374,9 +382,10 @@ class Tici(HardwareBase):
|
||||
|
||||
def set_power_save(self, powersave_enabled):
|
||||
# amplifier, 100mW at idle
|
||||
self.amplifier.set_global_shutdown(amp_disabled=powersave_enabled)
|
||||
if not powersave_enabled:
|
||||
self.amplifier.initialize_configuration(self.get_device_type())
|
||||
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 ***
|
||||
|
||||
@@ -414,7 +423,8 @@ class Tici(HardwareBase):
|
||||
return 0
|
||||
|
||||
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
|
||||
os.system("sudo chmod a+w /dev/kmsg")
|
||||
@@ -468,8 +478,9 @@ class Tici(HardwareBase):
|
||||
# use sim slot
|
||||
'AT^SIMSWAP=1',
|
||||
|
||||
# configure ECM mode
|
||||
'AT$QCPCFG=usbNet,1'
|
||||
# ethernet config
|
||||
'AT$QCPCFG=usbNet,0',
|
||||
'AT$QCNETDEVCTL=3,1',
|
||||
]
|
||||
else:
|
||||
cmds += [
|
||||
@@ -478,6 +489,12 @@ class Tici(HardwareBase):
|
||||
'AT+QNVFW="/nv/item_files/ims/IMS_enable",00',
|
||||
'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
|
||||
os.system('mmcli -m any --3gpp-set-initial-eps-bearer-settings="apn="')
|
||||
|
||||
66
system/hardware/tici/power_monitor.py
Normal file
66
system/hardware/tici/power_monitor.py
Normal 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}")
|
||||
9
system/hardware/tici/precise_power_measure.py
Normal file
9
system/hardware/tici/precise_power_measure.py
Normal 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}")
|
||||
18
system/hardware/tici/restart_modem.sh
Normal file
18
system/hardware/tici/restart_modem.sh
Normal 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
|
||||
0
system/hardware/tici/tests/__init__.py
Normal file
0
system/hardware/tici/tests/__init__.py
Normal file
73
system/hardware/tici/tests/compare_casync_manifest.py
Normal file
73
system/hardware/tici/tests/compare_casync_manifest.py
Normal 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}")
|
||||
26
system/hardware/tici/tests/test_agnos_updater.py
Normal file
26
system/hardware/tici/tests/test_agnos_updater.py
Normal 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()
|
||||
75
system/hardware/tici/tests/test_amplifier.py
Normal file
75
system/hardware/tici/tests/test_amplifier.py
Normal 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()
|
||||
153
system/hardware/tici/tests/test_casync.py
Normal file
153
system/hardware/tici/tests/test_casync.py
Normal 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()
|
||||
28
system/hardware/tici/tests/test_hardware.py
Normal file
28
system/hardware/tici/tests/test_hardware.py
Normal 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()
|
||||
134
system/hardware/tici/tests/test_power_draw.py
Normal file
134
system/hardware/tici/tests/test_power_draw.py
Normal 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()
|
||||
Reference in New Issue
Block a user