move some tools back to root (#38235)

This commit is contained in:
Adeeb Shihadeh
2026-06-24 19:21:19 -07:00
committed by GitHub
parent 7d325d6650
commit addca46f68
89 changed files with 82 additions and 74 deletions
View File
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
set -euo pipefail
# Forward all openpilot service ports
while IFS=' ' read -r name port; do
adb forward "tcp:${port}" "tcp:${port}" > /dev/null
done < <(python3 - <<'PY'
from openpilot.cereal.services import SERVICE_LIST
FNV_PRIME = 0x100000001b3
FNV_OFFSET_BASIS = 0xcbf29ce484222325
START_PORT = 8023
MAX_PORT = 65535
PORT_RANGE = MAX_PORT - START_PORT
MASK = 0xffffffffffffffff
def fnv1a(endpoint: str) -> int:
h = FNV_OFFSET_BASIS
for b in endpoint.encode():
h ^= b
h = (h * FNV_PRIME) & MASK
return h
ports = set()
for name in SERVICE_LIST.keys():
port = START_PORT + fnv1a(name) % PORT_RANGE
ports.add((name, port))
for name, port in sorted(ports):
print(f"{name} {port}")
PY
)
# Forward SSH port, finding a free local port if 2222 is taken.
SSH_PORT=2222
while ss -tln | grep -q ":${SSH_PORT} "; do
SSH_PORT=$((SSH_PORT + 1))
done
adb forward tcp:${SSH_PORT} tcp:22
# SSH!
ssh comma@localhost -p ${SSH_PORT} "$@"
+109
View File
@@ -0,0 +1,109 @@
#!/usr/bin/env python3
import argparse
import binascii
import time
from collections import defaultdict
import openpilot.cereal.messaging as messaging
from tools.scripts.car.can_table import can_table
from openpilot.tools.lib.logreader import LogIterable, LogReader
RED = '\033[91m'
CLEAR = '\033[0m'
def update(msgs, bus, dat, low_to_high, high_to_low, quiet=False):
for x in msgs:
if x.which() != 'can':
continue
for y in x.can:
if y.src == bus:
dat[y.address] = y.dat
i = int.from_bytes(y.dat, byteorder='big')
l_h = low_to_high[y.address]
h_l = high_to_low[y.address]
change = None
if (i | l_h) != l_h:
low_to_high[y.address] = i | l_h
change = "+"
if (~i | h_l) != h_l:
high_to_low[y.address] = ~i | h_l
change = "-"
if change and not quiet:
print(f"{time.monotonic():.2f}\t{hex(y.address)} ({y.address})\t{change}{binascii.hexlify(y.dat)}")
def can_printer(bus=0, init_msgs=None, new_msgs=None, table=False):
logcan = messaging.sub_sock('can', timeout=10)
dat = defaultdict(int)
low_to_high = defaultdict(int)
high_to_low = defaultdict(int)
if init_msgs is not None:
update(init_msgs, bus, dat, low_to_high, high_to_low, quiet=True)
low_to_high_init = low_to_high.copy()
high_to_low_init = high_to_low.copy()
if new_msgs is not None:
update(new_msgs, bus, dat, low_to_high, high_to_low)
else:
# Live mode
print(f"Waiting for messages on bus {bus}")
try:
while 1:
can_recv = messaging.drain_sock(logcan)
update(can_recv, bus, dat, low_to_high, high_to_low)
time.sleep(0.02)
except KeyboardInterrupt:
pass
print("\n\n")
tables = ""
for addr in sorted(dat.keys()):
init = low_to_high_init[addr] & high_to_low_init[addr]
now = low_to_high[addr] & high_to_low[addr]
d = now & ~init
if d == 0:
continue
b = d.to_bytes(len(dat[addr]), byteorder='big')
byts = ''.join([(c if c == '0' else f'{RED}{c}{CLEAR}') for c in str(binascii.hexlify(b))[2:-1]])
header = f"{hex(addr).ljust(6)}({str(addr).ljust(4)})"
print(header, byts)
tables += f"{header}\n"
tables += can_table(b) + "\n\n"
if table:
print(tables)
if __name__ == "__main__":
desc = """Collects messages and prints when a new bit transition is observed.
This is very useful to find signals based on user triggered actions, such as blinkers and seatbelt.
Leave the script running until no new transitions are seen, then perform the action."""
parser = argparse.ArgumentParser(description=desc,
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("--bus", type=int, help="CAN bus to print out", default=0)
parser.add_argument("--table", action="store_true", help="Print a cabana-like table")
parser.add_argument("init", type=str, nargs='?', help="Route or segment to initialize with. Use empty quotes to compare against all zeros.")
parser.add_argument("comp", type=str, nargs='?', help="Route or segment to compare against init")
args = parser.parse_args()
init_lr: LogIterable | None = None
new_lr: LogIterable | None = None
if args.init:
if args.init == '':
init_lr = []
else:
init_lr = LogReader(args.init)
if args.comp:
new_lr = LogReader(args.comp)
can_printer(args.bus, init_msgs=init_lr, new_msgs=new_lr, table=args.table)
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env python3
import argparse
import binascii
import time
from collections import defaultdict
import openpilot.cereal.messaging as messaging
def can_printer(bus, max_msg, addr, ascii_decode):
logcan = messaging.sub_sock('can', addr=addr)
start = time.monotonic()
lp = time.monotonic()
msgs = defaultdict(list)
while 1:
can_recv = messaging.drain_sock(logcan, wait_for_one=True)
for x in can_recv:
for y in x.can:
if y.src == bus:
msgs[y.address].append(y.dat)
if time.monotonic() - lp > 0.1:
dd = chr(27) + "[2J"
dd += f"{time.monotonic() - start:5.2f}\n"
for _addr in sorted(msgs.keys()):
a = f"\"{msgs[_addr][-1].decode('ascii', 'backslashreplace')}\"" if ascii_decode else ""
x = binascii.hexlify(msgs[_addr][-1]).decode('ascii')
freq = len(msgs[_addr]) / (time.monotonic() - start)
if max_msg is None or _addr < max_msg:
dd += f"{_addr:04X}({_addr:4d})({len(msgs[_addr]):6d})({freq:3}dHz) {x.ljust(20)} {a}\n"
print(dd)
lp = time.monotonic()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="simple CAN data viewer",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("--bus", type=int, help="CAN bus to print out", default=0)
parser.add_argument("--max_msg", type=int, help="max addr")
parser.add_argument("--ascii", action='store_true', help="decode as ascii")
parser.add_argument("--addr", default="127.0.0.1")
args = parser.parse_args()
can_printer(args.bus, args.max_msg, args.addr, args.ascii)
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env python3
import argparse
import pandas as pd
import openpilot.cereal.messaging as messaging
def can_table(dat):
rows = []
for b in dat:
r = list(bin(b).lstrip('0b').zfill(8))
r += [hex(b)]
rows.append(r)
df = pd.DataFrame(data=rows)
df.columns = [str(n) for n in range(7, -1, -1)] + [' ']
table = df.to_markdown(tablefmt='grid')
return table
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Cabana-like table of bits for your terminal",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("addr", type=str, nargs=1)
parser.add_argument("bus", type=int, default=0, nargs='?')
args = parser.parse_args()
addr = int(args.addr[0], 0)
can = messaging.sub_sock('can', conflate=False, timeout=None)
print(f"waiting for {hex(addr)} ({addr}) on bus {args.bus}...")
latest = None
while True:
for msg in messaging.drain_sock(can, wait_for_one=True):
for m in msg.can:
if m.address == addr and m.src == args.bus:
latest = m
if latest is None:
continue
table = can_table(latest.dat)
print(f"\n\n{hex(addr)} ({addr}) on bus {args.bus}\n{table}")
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env python3
import sys
import argparse
from subprocess import check_output, CalledProcessError
from opendbc.car.carlog import carlog
from opendbc.car.uds import UdsClient, MessageTimeoutError, SESSION_TYPE, DTC_GROUP_TYPE
from opendbc.car.structs import CarParams
from panda import Panda
parser = argparse.ArgumentParser(description="clear DTC status")
parser.add_argument("addr", type=lambda x: int(x,0), nargs="?", default=0x7DF) # default is functional (broadcast) address
parser.add_argument("--bus", type=int, default=0)
parser.add_argument('--debug', action='store_true')
args = parser.parse_args()
if args.debug:
carlog.setLevel('DEBUG')
try:
check_output(["pidof", "pandad"])
print("pandad is running, please kill openpilot before running this script! (aborted)")
sys.exit(1)
except CalledProcessError as e:
if e.returncode != 1: # 1 == no process found (pandad not running)
raise e
panda = Panda()
panda.set_safety_mode(CarParams.SafetyModel.elm327)
uds_client = UdsClient(panda, args.addr, bus=args.bus)
print("extended diagnostic session ...")
try:
uds_client.diagnostic_session_control(SESSION_TYPE.EXTENDED_DIAGNOSTIC)
except MessageTimeoutError:
# functional address isn't properly handled so a timeout occurs
if args.addr != 0x7DF:
raise
print("clear diagnostic info ...")
try:
uds_client.clear_diagnostic_information(DTC_GROUP_TYPE.ALL)
except MessageTimeoutError:
# functional address isn't properly handled so a timeout occurs
if args.addr != 0x7DF:
pass
print("")
print("you may need to power cycle your vehicle now")
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env python3
import time
import openpilot.cereal.messaging as messaging
from opendbc.car.disable_ecu import disable_ecu
from openpilot.selfdrive.car.card import can_comm_callbacks
if __name__ == "__main__":
sendcan = messaging.pub_sock('sendcan')
logcan = messaging.sub_sock('can')
can_callbacks = can_comm_callbacks(logcan, sendcan)
time.sleep(1)
# honda bosch radar disable
disabled = disable_ecu(*can_callbacks, bus=1, addr=0x18DAB0F1, com_cont_req=b'\x28\x83\x03', timeout=0.5)
print(f"disabled: {disabled}")
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env python3
import argparse
import time
import openpilot.cereal.messaging as messaging
from opendbc.car.carlog import carlog
from opendbc.car.ecu_addrs import get_all_ecu_addrs
from openpilot.common.params import Params
from openpilot.selfdrive.car.card import can_comm_callbacks, obd_callback
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Get addresses of all ECUs')
parser.add_argument('--debug', action='store_true')
parser.add_argument('--bus', type=int, default=1)
parser.add_argument('--no-obd', action='store_true')
parser.add_argument('--timeout', type=float, default=1.0)
args = parser.parse_args()
if args.debug:
carlog.setLevel('DEBUG')
logcan = messaging.sub_sock('can')
sendcan = messaging.pub_sock('sendcan')
can_callbacks = can_comm_callbacks(logcan, sendcan)
# Set up params for pandad
params = Params()
params.remove("FirmwareQueryDone")
params.put_bool("IsOffroad", True, block=True)
time.sleep(0.2) # thread is 10 Hz
params.put_bool("IsOffroad", False, block=True)
obd_callback(params)(not args.no_obd)
print("Getting ECU addresses ...")
ecu_addrs = get_all_ecu_addrs(*can_callbacks, args.bus, args.timeout)
print()
print("Found ECUs on rx addresses:")
for addr, subaddr, _ in ecu_addrs:
msg = f" {hex(addr)}"
if subaddr is not None:
msg += f" (sub-address: {hex(subaddr)})"
print(msg)
+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/env python3
import time
import argparse
import openpilot.cereal.messaging as messaging
from opendbc.car.structs import car
from opendbc.car.carlog import carlog
from opendbc.car.fw_versions import get_fw_versions, match_fw_to_car
from opendbc.car.vin import get_vin
from openpilot.common.params import Params
from openpilot.selfdrive.car.card import can_comm_callbacks, obd_callback
from typing import Any
Ecu = car.CarParams.Ecu
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Get firmware version of ECUs')
parser.add_argument('--scan', action='store_true')
parser.add_argument('--debug', action='store_true')
parser.add_argument('--brand', help='Only query addresses/with requests for this brand')
args = parser.parse_args()
if args.debug:
carlog.setLevel('DEBUG')
logcan = messaging.sub_sock('can')
pandaStates_sock = messaging.sub_sock('pandaStates')
sendcan = messaging.pub_sock('sendcan')
can_callbacks = can_comm_callbacks(logcan, sendcan)
# Set up params for pandad
params = Params()
params.remove("FirmwareQueryDone")
params.put_bool("IsOffroad", True, block=True)
time.sleep(0.2) # thread is 10 Hz
params.put_bool("IsOffroad", False, block=True)
set_obd_multiplexing = obd_callback(params)
extra: Any = None
if args.scan:
extra = {}
# Honda
for i in range(256):
extra[(Ecu.unknown, 0x18da00f1 + (i << 8), None)] = []
extra[(Ecu.unknown, 0x700 + i, None)] = []
extra[(Ecu.unknown, 0x750, i)] = []
extra = {"any": {"debug": extra}}
t = time.monotonic()
print("Getting vin...")
set_obd_multiplexing(True)
vin_rx_addr, vin_rx_bus, vin = get_vin(*can_callbacks, (0, 1))
print(f'RX: {hex(vin_rx_addr)}, BUS: {vin_rx_bus}, VIN: {vin}')
print(f"Getting VIN took {time.monotonic() - t:.3f} s")
print()
t = time.monotonic()
fw_vers = get_fw_versions(*can_callbacks, set_obd_multiplexing, query_brand=args.brand, extra=extra, progress=True)
_, candidates = match_fw_to_car(fw_vers, vin)
print()
print("Found FW versions")
print("{")
padding = max([len(fw.brand) for fw in fw_vers] or [0])
for version in fw_vers:
subaddr = None if version.subAddress == 0 else hex(version.subAddress)
print(f" Brand: {version.brand:{padding}}, bus: {version.bus}, OBD: {version.obdMultiplexing} - " +
f"(Ecu.{version.ecu}, {hex(version.address)}, {subaddr}): [{version.fwVersion!r}]")
print("}")
print()
print("Possible matches:", candidates)
print(f"Getting fw took {time.monotonic() - t:.3f} s")
+135
View File
@@ -0,0 +1,135 @@
#!/usr/bin/env python3
"""Some Hyundai radars can be reconfigured to output (debug) radar points on bus 1.
Reconfiguration is done over UDS by reading/writing to 0x0142 using the Read/Write Data By Identifier
endpoints (0x22 & 0x2E). This script checks your radar firmware version against a list of known
firmware versions. If you want to try on a new radar make sure to note the default config value
in case it's different from the other radars and you need to revert the changes.
After changing the config the car should not show any faults when openpilot is not running.
These config changes are persistent across car reboots. You need to run this script again
to go back to the default values.
USE AT YOUR OWN RISK! Safety features, like AEB and FCW, might be affected by these changes."""
import sys
import argparse
from typing import NamedTuple
from subprocess import check_output, CalledProcessError
from opendbc.car.carlog import carlog
from opendbc.car.uds import UdsClient, SESSION_TYPE, DATA_IDENTIFIER_TYPE
from opendbc.car.structs import CarParams
from panda.python import Panda
class ConfigValues(NamedTuple):
default_config: bytes
tracks_enabled: bytes
# If your radar supports changing data identifier 0x0142 as well make a PR to
# this file to add your firmware version. Make sure to post a drive as proof!
# NOTE: these firmware versions do not match what openpilot uses
# because this script uses a different diagnostic session type
SUPPORTED_FW_VERSIONS = {
# 2020 SONATA
b"DN8_ SCC FHCUP 1.00 1.00 99110-L0000\x19\x08)\x15T ": ConfigValues(
default_config=b"\x00\x00\x00\x01\x00\x00",
tracks_enabled=b"\x00\x00\x00\x01\x00\x01"),
b"DN8_ SCC F-CUP 1.00 1.00 99110-L0000\x19\x08)\x15T ": ConfigValues(
default_config=b"\x00\x00\x00\x01\x00\x00",
tracks_enabled=b"\x00\x00\x00\x01\x00\x01"),
# 2021 SONATA HYBRID
b"DNhe SCC FHCUP 1.00 1.00 99110-L5000\x19\x04&\x13' ": ConfigValues(
default_config=b"\x00\x00\x00\x01\x00\x00",
tracks_enabled=b"\x00\x00\x00\x01\x00\x01"),
b"DNhe SCC FHCUP 1.00 1.02 99110-L5000 \x01#\x15# ": ConfigValues(
default_config=b"\x00\x00\x00\x01\x00\x00",
tracks_enabled=b"\x00\x00\x00\x01\x00\x01"),
# 2020 PALISADE
b"LX2_ SCC FHCUP 1.00 1.04 99110-S8100\x19\x05\x02\x16V ": ConfigValues(
default_config=b"\x00\x00\x00\x01\x00\x00",
tracks_enabled=b"\x00\x00\x00\x01\x00\x01"),
# 2022 PALISADE
b"LX2_ SCC FHCUP 1.00 1.00 99110-S8110!\x04\x05\x17\x01 ": ConfigValues(
default_config=b"\x00\x00\x00\x01\x00\x00",
tracks_enabled=b"\x00\x00\x00\x01\x00\x01"),
# 2020 SANTA FE
b"TM__ SCC F-CUP 1.00 1.03 99110-S2000\x19\x050\x13' ": ConfigValues(
default_config=b"\x00\x00\x00\x01\x00\x00",
tracks_enabled=b"\x00\x00\x00\x01\x00\x01"),
# 2020 GENESIS G70
b'IK__ SCC F-CUP 1.00 1.02 96400-G9100\x18\x07\x06\x17\x12 ': ConfigValues(
default_config=b"\x00\x00\x00\x01\x00\x00",
tracks_enabled=b"\x00\x00\x00\x01\x00\x01"),
# 2019 SANTA FE
b"TM__ SCC F-CUP 1.00 1.00 99110-S1210\x19\x01%\x168 ": ConfigValues(
default_config=b"\x00\x00\x00\x01\x00\x00",
tracks_enabled=b"\x00\x00\x00\x01\x00\x01"),
b"TM__ SCC F-CUP 1.00 1.02 99110-S2000\x18\x07\x08\x18W ": ConfigValues(
default_config=b"\x00\x00\x00\x01\x00\x00",
tracks_enabled=b"\x00\x00\x00\x01\x00\x01"),
# 2021 K5 HEV
b"DLhe SCC FHCUP 1.00 1.02 99110-L7000 \x01 \x102 ": ConfigValues(
default_config=b"\x00\x00\x00\x01\x00\x00",
tracks_enabled=b"\x00\x00\x00\x01\x00\x01"),
}
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='configure radar to output points (or reset to default)')
parser.add_argument('--default', action="store_true", default=False, help='reset to default configuration (default: false)')
parser.add_argument('--debug', action="store_true", default=False, help='enable debug output (default: false)')
parser.add_argument('--bus', type=int, default=0, help='can bus to use (default: 0)')
args = parser.parse_args()
if args.debug:
carlog.setLevel('DEBUG')
try:
check_output(["pidof", "pandad"])
print("pandad is running, please kill openpilot before running this script! (aborted)")
sys.exit(1)
except CalledProcessError as e:
if e.returncode != 1: # 1 == no process found (pandad not running)
raise e
confirm = input("power on the vehicle keeping the engine off (press start button twice) then type OK to continue: ").upper().strip()
if confirm != "OK":
print("\nyou didn't type 'OK! (aborted)")
sys.exit(0)
panda = Panda()
panda.set_safety_mode(CarParams.SafetyModel.elm327)
uds_client = UdsClient(panda, 0x7D0, bus=args.bus)
print("\n[START DIAGNOSTIC SESSION]")
session_type : SESSION_TYPE = 0x07
uds_client.diagnostic_session_control(session_type)
print("[HARDWARE/SOFTWARE VERSION]")
fw_version_data_id : DATA_IDENTIFIER_TYPE = 0xf100
fw_version = uds_client.read_data_by_identifier(fw_version_data_id)
print(fw_version)
if fw_version not in SUPPORTED_FW_VERSIONS.keys():
print("radar not supported! (aborted)")
sys.exit(1)
print("[GET CONFIGURATION]")
config_data_id : DATA_IDENTIFIER_TYPE = 0x0142
current_config = uds_client.read_data_by_identifier(config_data_id)
config_values = SUPPORTED_FW_VERSIONS[fw_version]
new_config = config_values.default_config if args.default else config_values.tracks_enabled
print(f"current config: 0x{current_config.hex()}")
if current_config != new_config:
print("[CHANGE CONFIGURATION]")
print(f"new config: 0x{new_config.hex()}")
uds_client.write_data_by_identifier(config_data_id, new_config)
if not args.default and current_config != SUPPORTED_FW_VERSIONS[fw_version].default_config:
print("\ncurrent config does not match expected default! (aborted)")
sys.exit(1)
print("[DONE]")
print("\nrestart your vehicle and ensure there are no faults")
if not args.default:
print("you can run this script again with --default to go back to the original (factory) settings")
else:
print("[DONE]")
print("\ncurrent config is already the desired configuration")
sys.exit(0)
+131
View File
@@ -0,0 +1,131 @@
#!/usr/bin/env python3
import argparse
import numpy as np
import matplotlib.pyplot as plt
from functools import partial
from tqdm import tqdm
from typing import NamedTuple
from openpilot.tools.lib.logreader import LogReader
from openpilot.selfdrive.locationd.models.pose_kf import EARTH_G
RLOG_MIN_LAT_ACTIVE = 50
RLOG_MIN_STEERING_UNPRESSED = 50
RLOG_MIN_REQUESTING_MAX = 25 # sample many times after reaching max torque
QLOG_DECIMATION = 10
class Event(NamedTuple):
lateral_accel: float
speed: float
roll: float
timestamp: float # relative to start of route (s)
def find_events(lr: LogReader, extrapolate: bool = False, qlog: bool = False) -> list[Event]:
min_lat_active = RLOG_MIN_LAT_ACTIVE // QLOG_DECIMATION if qlog else RLOG_MIN_LAT_ACTIVE
min_steering_unpressed = RLOG_MIN_STEERING_UNPRESSED // QLOG_DECIMATION if qlog else RLOG_MIN_STEERING_UNPRESSED
min_requesting_max = RLOG_MIN_REQUESTING_MAX // QLOG_DECIMATION if qlog else RLOG_MIN_REQUESTING_MAX
# if we test with driver torque safety, max torque can be slightly noisy
steer_threshold = 0.7 if extrapolate else 0.95
events = []
# state tracking
steering_unpressed = 0 # frames
requesting_max = 0 # frames
lat_active = 0 # frames
# current state
curvature = 0
v_ego = 0
roll = 0
out_torque = 0
start_ts = 0
for msg in lr:
if msg.which() == 'carControl':
if start_ts == 0:
start_ts = msg.logMonoTime
lat_active = lat_active + 1 if msg.carControl.latActive else 0
elif msg.which() == 'carOutput':
out_torque = msg.carOutput.actuatorsOutput.torque
requesting_max = requesting_max + 1 if abs(out_torque) > steer_threshold else 0
elif msg.which() == 'carState':
steering_unpressed = steering_unpressed + 1 if not msg.carState.steeringPressed else 0
v_ego = msg.carState.vEgo
elif msg.which() == 'controlsState':
curvature = msg.controlsState.curvature
elif msg.which() == 'liveParameters':
roll = msg.liveParameters.roll
if lat_active > min_lat_active and steering_unpressed > min_steering_unpressed and requesting_max > min_requesting_max:
# TODO: record max lat accel at the end of the event, need to use the past lat accel as overriding can happen before we detect it
requesting_max = 0
factor = 1 / abs(out_torque)
current_lateral_accel = (curvature * v_ego ** 2 * factor) - roll * EARTH_G
events.append(Event(current_lateral_accel, v_ego, roll, round((msg.logMonoTime - start_ts) * 1e-9, 2)))
print(events[-1])
return events
if __name__ == '__main__':
parser = argparse.ArgumentParser(description="Find max lateral acceleration events",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("route", nargs='+')
parser.add_argument("-e", "--extrapolate", action="store_true", help="Extrapolates max lateral acceleration events linearly. " +
"This option can be far less accurate.")
args = parser.parse_args()
events = []
for route in tqdm(args.route):
try:
lr = LogReader(route, sort_by_time=True)
except Exception:
print(f'Skipping {route}')
continue
qlog = route.endswith('/q')
if qlog:
print('WARNING: Treating route as qlog!')
print('Finding events...')
events += lr.run_across_segments(8, partial(find_events, extrapolate=args.extrapolate, qlog=qlog), disable_tqdm=True)
print()
print(f'Found {len(events)} events')
perc_left_accel = -np.percentile([-ev.lateral_accel for ev in events if ev.lateral_accel < 0] or [0], 90)
perc_right_accel = np.percentile([ev.lateral_accel for ev in events if ev.lateral_accel > 0] or [0], 90)
CP = lr.first('carParams')
plt.ion()
plt.clf()
plt.suptitle(f'{CP.carFingerprint} - Max lateral acceleration events')
plt.title(', '.join(args.route))
plt.scatter([ev.speed for ev in events], [ev.lateral_accel for ev in events], label='max lateral accel events')
plt.plot([0, 35], [3, 3], c='r', label='ISO 11270 - 3 m/s^2')
plt.plot([0, 35], [-3, -3], c='r')
plt.plot([0, 35], [perc_left_accel, perc_left_accel], c='g', linestyle='--', label='90th percentile left lateral accel')
plt.plot([0, 35], [perc_right_accel, perc_right_accel], c='#ff7f0e', linestyle='--', label='90th percentile right lateral accel')
plt.text(0.4, float(perc_left_accel + 0.4), f'{perc_left_accel:.2f} m/s^2', verticalalignment='center', fontsize=12)
plt.text(0.4, float(perc_right_accel - 0.4), f'{perc_right_accel:.2f} m/s^2', verticalalignment='center', fontsize=12)
plt.xlim(0, 35)
plt.ylim(-5, 5)
plt.xlabel('speed (m/s)')
plt.ylabel('lateral acceleration (m/s^2)')
plt.legend()
plt.show(block=True)
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env python3
import os
import argparse
import struct
from collections import deque
from statistics import mean
from openpilot.cereal import log
import openpilot.cereal.messaging as messaging
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Sniff a communication socket')
parser.add_argument('--addr', default='127.0.0.1')
args = parser.parse_args()
if args.addr != "127.0.0.1":
os.environ["ZMQ"] = "1"
messaging.reset_context()
poller = messaging.Poller()
messaging.sub_sock('can', poller, addr=args.addr)
active = 0
start_t = 0
start_v = 0
max_v = 0
max_t = 0
window = deque(maxlen=10)
avg = 0
while 1:
polld = poller.poll(1000)
for sock in polld:
msg = sock.receive()
with log.Event.from_bytes(msg) as log_evt:
evt = log_evt
for item in evt.can:
if item.address == 0xe4 and item.src == 128:
torque_req = struct.unpack('!h', item.dat[0:2])[0]
# print(torque_req)
active = abs(torque_req) > 0
if abs(torque_req) < 100:
if max_v > 5:
print(f'{start_v} -> {max_v} = {round(max_v - start_v, 2)} over {round(max_t - start_t, 2)}s')
start_t = evt.logMonoTime / 1e9
start_v = avg
max_t = 0
max_v = 0
if item.address == 0x1ab and item.src == 0:
motor_torque = ((item.dat[0] & 0x3) << 8) + item.dat[1]
window.append(motor_torque)
avg = mean(window)
#print(f'{evt.logMonoTime}: {avg}')
if active and avg > max_v + 0.5:
max_v = avg
max_t = evt.logMonoTime / 1e9
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env python3
import sys
import argparse
from subprocess import check_output, CalledProcessError
from opendbc.car.carlog import carlog
from opendbc.car.uds import UdsClient, SESSION_TYPE, DTC_REPORT_TYPE, DTC_STATUS_MASK_TYPE, get_dtc_num_as_str, get_dtc_status_names
from opendbc.car.structs import CarParams
from panda import Panda
parser = argparse.ArgumentParser(description="read DTC status")
parser.add_argument("addr", type=lambda x: int(x,0))
parser.add_argument("--bus", type=int, default=0)
parser.add_argument('--debug', action='store_true')
args = parser.parse_args()
if args.debug:
carlog.setLevel('DEBUG')
try:
check_output(["pidof", "pandad"])
print("pandad is running, please kill openpilot before running this script! (aborted)")
sys.exit(1)
except CalledProcessError as e:
if e.returncode != 1: # 1 == no process found (pandad not running)
raise e
panda = Panda()
panda.set_safety_mode(CarParams.SafetyModel.elm327)
uds_client = UdsClient(panda, args.addr, bus=args.bus)
print("extended diagnostic session ...")
uds_client.diagnostic_session_control(SESSION_TYPE.EXTENDED_DIAGNOSTIC)
print("read diagnostic codes ...")
data = uds_client.read_dtc_information(DTC_REPORT_TYPE.DTC_BY_STATUS_MASK, DTC_STATUS_MASK_TYPE.ALL)
print("status availability:", " ".join(get_dtc_status_names(data[0])))
print("DTC status:")
for i in range(1, len(data), 4):
dtc_num = get_dtc_num_as_str(data[i:i+3])
dtc_status = " ".join(get_dtc_status_names(data[i+3]))
print(dtc_num, dtc_status)
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env python3
import sys
import numpy as np
import matplotlib.pyplot as plt
from sklearn import linear_model
from opendbc.car.toyota.values import STEER_THRESHOLD
from openpilot.tools.lib.logreader import LogReader
MIN_SAMPLES = 30 * 100
def to_signed(n, bits):
if n >= (1 << max((bits - 1), 0)):
n = n - (1 << max(bits, 0))
return n
def get_eps_factor(lr, plot=False):
engaged = False
steering_pressed = False
torque_cmd, eps_torque = None, None
cmds, eps = [], []
for msg in lr:
if msg.which() != 'can':
continue
for m in msg.can:
if m.address == 0x2e4 and m.src == 128:
engaged = bool(m.dat[0] & 1)
torque_cmd = to_signed((m.dat[1] << 8) | m.dat[2], 16)
elif m.address == 0x260 and m.src == 0:
eps_torque = to_signed((m.dat[5] << 8) | m.dat[6], 16)
steering_pressed = abs(to_signed((m.dat[1] << 8) | m.dat[2], 16)) > STEER_THRESHOLD
if engaged and torque_cmd is not None and eps_torque is not None and not steering_pressed:
cmds.append(torque_cmd)
eps.append(eps_torque)
else:
if len(cmds) > MIN_SAMPLES:
break
cmds, eps = [], []
if len(cmds) < MIN_SAMPLES:
raise Exception("too few samples found in route")
lm = linear_model.LinearRegression(fit_intercept=False)
lm.fit(np.array(cmds).reshape(-1, 1), eps)
scale_factor = 1. / lm.coef_[0]
if plot:
plt.plot(np.array(eps) * scale_factor)
plt.plot(cmds)
plt.show()
return scale_factor
if __name__ == "__main__":
lr = LogReader(sys.argv[1])
n = get_eps_factor(lr, plot="--plot" in sys.argv)
print("EPS torque factor: ", n)
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env python3
import argparse
import time
import openpilot.cereal.messaging as messaging
from opendbc.car.carlog import carlog
from opendbc.car.vin import get_vin
from openpilot.selfdrive.car.card import can_comm_callbacks
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Get VIN of the car')
parser.add_argument('--debug', action='store_true')
parser.add_argument('--bus', type=int, default=1)
parser.add_argument('--timeout', type=float, default=0.1)
parser.add_argument('--retry', type=int, default=5)
args = parser.parse_args()
if args.debug:
carlog.setLevel('DEBUG')
sendcan = messaging.pub_sock('sendcan')
logcan = messaging.sub_sock('can')
can_callbacks = can_comm_callbacks(logcan, sendcan)
time.sleep(1)
vin_rx_addr, vin_rx_bus, vin = get_vin(*can_callbacks, (args.bus,), args.timeout, args.retry)
print(f'RX: {hex(vin_rx_addr)}, BUS: {vin_rx_bus}, VIN: {vin}')
+164
View File
@@ -0,0 +1,164 @@
#!/usr/bin/env python3
import argparse
import struct
from enum import IntEnum
from opendbc.car.carlog import carlog
from opendbc.car.uds import UdsClient, MessageTimeoutError, NegativeResponseError, SESSION_TYPE,\
DATA_IDENTIFIER_TYPE, ACCESS_TYPE
from opendbc.car.structs import CarParams
from panda import Panda
from datetime import date
# TODO: extend UDS library to allow custom/vendor-defined data identifiers without ignoring type checks
class VOLKSWAGEN_DATA_IDENTIFIER_TYPE(IntEnum):
CODING = 0x0600
# TODO: extend UDS library security_access() to take an access level offset per ISO 14229-1:2020 10.4 and remove this
class ACCESS_TYPE_LEVEL_1(IntEnum):
REQUEST_SEED = ACCESS_TYPE.REQUEST_SEED + 2
SEND_KEY = ACCESS_TYPE.SEND_KEY + 2
MQB_EPS_CAN_ADDR = 0x712
RX_OFFSET = 0x6a
if __name__ == "__main__":
desc_text = "Shows Volkswagen EPS software and coding info, and enables or disables Heading Control Assist " + \
"(Lane Assist). Useful for enabling HCA on cars without factory Lane Assist that want to use " + \
"openpilot integrated at the CAN gateway (J533)."
epilog_text = "This tool is meant to run directly on a vehicle-installed comma three, with the " + \
"openpilot/tmux processes stopped. It should also work on a separate PC with a USB-attached comma " + \
"panda. Vehicle ignition must be on. Recommend engine not be running when making changes. Must " + \
"turn ignition off and on again for any changes to take effect."
parser = argparse.ArgumentParser(description=desc_text, epilog=epilog_text)
parser.add_argument("--debug", action="store_true", help="enable ISO-TP/UDS stack debugging output")
parser.add_argument("action", choices={"show", "enable", "disable"}, help="show or modify current EPS HCA config")
args = parser.parse_args()
if args.debug:
carlog.setLevel('DEBUG')
panda = Panda()
panda.set_safety_mode(CarParams.SafetyModel.elm327)
uds_client = UdsClient(panda, MQB_EPS_CAN_ADDR, MQB_EPS_CAN_ADDR + RX_OFFSET, 1, timeout=0.2)
try:
uds_client.diagnostic_session_control(SESSION_TYPE.EXTENDED_DIAGNOSTIC)
except MessageTimeoutError:
print("Timeout opening session with EPS")
quit()
odx_file, current_coding = None, None
try:
hw_pn = uds_client.read_data_by_identifier(DATA_IDENTIFIER_TYPE.VEHICLE_MANUFACTURER_ECU_HARDWARE_NUMBER).decode("utf-8")
sw_pn = uds_client.read_data_by_identifier(DATA_IDENTIFIER_TYPE.VEHICLE_MANUFACTURER_SPARE_PART_NUMBER).decode("utf-8")
sw_ver = uds_client.read_data_by_identifier(DATA_IDENTIFIER_TYPE.VEHICLE_MANUFACTURER_ECU_SOFTWARE_VERSION_NUMBER).decode("utf-8")
component = uds_client.read_data_by_identifier(DATA_IDENTIFIER_TYPE.SYSTEM_NAME_OR_ENGINE_TYPE).decode("utf-8")
odx_file = uds_client.read_data_by_identifier(DATA_IDENTIFIER_TYPE.ODX_FILE).decode("utf-8").rstrip('\x00')
current_coding = uds_client.read_data_by_identifier(VOLKSWAGEN_DATA_IDENTIFIER_TYPE.CODING)
coding_text = current_coding.hex()
print("\nEPS diagnostic data\n")
print(f" Part No HW: {hw_pn}")
print(f" Part No SW: {sw_pn}")
print(f" SW Version: {sw_ver}")
print(f" Component: {component}")
print(f" Coding: {coding_text}")
print(f" ASAM Dataset: {odx_file}")
except NegativeResponseError:
print("Error fetching data from EPS")
quit()
except MessageTimeoutError:
print("Timeout fetching data from EPS")
quit()
coding_variant, current_coding_array, coding_byte, coding_bit = None, None, 0, 0
coding_length = len(current_coding)
# EPS_MQB_ZFLS
if odx_file in ("EV_SteerAssisMQB", "EV_SteerAssisMNB"):
coding_variant = "ZFLS"
coding_byte = 0
coding_bit = 4
# MQB_PP_APA, MQB_VWBS_GEN2
elif odx_file in ("EV_SteerAssisVWBSMQBA", "EV_SteerAssisVWBSMQBGen2"):
coding_variant = "APA"
coding_byte = 3
coding_bit = 0
else:
print("Configuration changes not yet supported on this EPS!")
quit()
current_coding_array = struct.unpack(f"!{coding_length}B", current_coding)
hca_enabled = (current_coding_array[coding_byte] & (1 << coding_bit) != 0)
hca_text = ("DISABLED", "ENABLED")[hca_enabled]
print(f" Lane Assist: {hca_text}")
try:
params = uds_client.read_data_by_identifier(DATA_IDENTIFIER_TYPE.APPLICATION_DATA_IDENTIFICATION).decode("utf-8")
param_version_system_params = params[1:3]
param_vehicle_type = params[3:5]
param_index_char_curve = params[5:7]
param_version_char_values = params[7:9]
param_version_memory_map = params[9:11]
print("\nEPS parameterization (per-vehicle calibration) data\n")
print(f" Version of system parameters: {param_version_system_params}")
print(f" Vehicle type: {param_vehicle_type}")
print(f" Index of characteristic curve: {param_index_char_curve}")
print(f" Version of characteristic values: {param_version_char_values}")
print(f" Version of memory map: {param_version_memory_map}")
except (NegativeResponseError, MessageTimeoutError):
print("Error fetching parameterization data from EPS!")
quit()
if args.action in ["enable", "disable"]:
print("\nAttempting configuration update")
assert(coding_variant in ("ZFLS", "APA"))
# ZFLS EPS config coding length can be anywhere from 1 to 4 bytes, but the
# bit we care about is always in the same place in the first byte
if args.action == "enable":
new_byte = current_coding_array[coding_byte] | (1 << coding_bit)
else:
new_byte = current_coding_array[coding_byte] & ~(1 << coding_bit)
new_coding = current_coding[0:coding_byte] + new_byte.to_bytes(1, "little") + current_coding[coding_byte+1:]
try:
seed = uds_client.security_access(ACCESS_TYPE_LEVEL_1.REQUEST_SEED)
key = struct.unpack("!I", seed)[0] + 28183 # yeah, it's like that
uds_client.security_access(ACCESS_TYPE_LEVEL_1.SEND_KEY, struct.pack("!I", key))
except (NegativeResponseError, MessageTimeoutError):
print("Security access failed!")
print("Open the hood and retry (disables the \"diagnostic firewall\" on newer vehicles)")
quit()
try:
# Programming date and tester number must be written before making
# a change, or write to CODING will fail with request sequence error
# Encoding on tester is unclear, it contains the workshop code in the
# last two bytes, but not the VZ/importer or tester serial number
# Can't seem to read it back, but we can read the calibration tester,
# so fib a little and say that same tester did the programming
current_date = date.today()
formatted_date = current_date.strftime('%y-%m-%d')
year, month, day = (int(part) for part in formatted_date.split('-'))
prog_date = bytes([year, month, day])
uds_client.write_data_by_identifier(DATA_IDENTIFIER_TYPE.PROGRAMMING_DATE, prog_date)
tester_num = uds_client.read_data_by_identifier(DATA_IDENTIFIER_TYPE.CALIBRATION_REPAIR_SHOP_CODE_OR_CALIBRATION_EQUIPMENT_SERIAL_NUMBER)
uds_client.write_data_by_identifier(DATA_IDENTIFIER_TYPE.REPAIR_SHOP_CODE_OR_TESTER_SERIAL_NUMBER, tester_num)
uds_client.write_data_by_identifier(VOLKSWAGEN_DATA_IDENTIFIER_TYPE.CODING, new_coding)
except (NegativeResponseError, MessageTimeoutError):
print("Writing new configuration failed!")
print("Make sure the comma processes are stopped: tmux kill-session -t comma")
quit()
try:
# Read back result just to make 100% sure everything worked
current_coding_text = uds_client.read_data_by_identifier(VOLKSWAGEN_DATA_IDENTIFIER_TYPE.CODING).hex()
print(f" New coding: {current_coding_text}")
except (NegativeResponseError, MessageTimeoutError):
print("Reading back updated coding failed!")
quit()
print("EPS configuration successfully updated")
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env python3
import sys
import math
import datetime
from collections import Counter
from pprint import pprint
from typing import cast
from openpilot.cereal.services import SERVICE_LIST
from openpilot.tools.lib.logreader import LogReader, ReadMode
from openpilot.selfdrive.test.process_replay.migration import migrate_all
if __name__ == "__main__":
cnt_events: Counter = Counter()
cams = [s for s in SERVICE_LIST if s.endswith('CameraState')]
cnt_cameras = dict.fromkeys(cams, 0)
events: list[tuple[float, set[str]]] = []
alerts: list[tuple[float, str]] = []
start_time = math.inf
end_time = -math.inf
ignition_off = None
for msg in migrate_all(LogReader(sys.argv[1], ReadMode.QLOG)):
t = (msg.logMonoTime - start_time) / 1e9
end_time = max(end_time, msg.logMonoTime)
start_time = min(start_time, msg.logMonoTime)
if msg.which() == 'onroadEvents':
for e in msg.onroadEvents:
cnt_events[e.name] += 1
ae = {str(e.name) for e in msg.onroadEvents if e.name not in ('pedalPressed', 'steerOverride', 'gasPressedOverride')}
if len(events) == 0 or ae != events[-1][1]:
events.append((t, ae))
elif msg.which() == 'selfdriveState':
at = msg.selfdriveState.alertType
if "/override" not in at or "lanechange" in at.lower():
if len(alerts) == 0 or alerts[-1][1] != at:
alerts.append((t, at))
elif msg.which() == 'pandaStates':
if ignition_off is None:
ign = any(ps.ignitionLine or ps.ignitionCan for ps in msg.pandaStates)
if not ign:
ignition_off = msg.logMonoTime
break
elif msg.which() in cams:
cnt_cameras[msg.which()] += 1
duration = (end_time - start_time) / 1e9
print("Events")
pprint(cnt_events)
print("\n")
print("Events")
for t, evt in events:
print(f"{t:8.2f} {evt}")
print("\n")
print("Cameras")
for k, v in cnt_cameras.items():
s = SERVICE_LIST[k]
expected_frames = int(s.frequency * duration / cast(float, s.decimation))
print(" ", k.ljust(20), f"{v}, {v/expected_frames:.1%} of expected")
print("\n")
print("Alerts")
for t, a in alerts:
print(f"{t:8.2f} {a}")
print("\n")
if ignition_off is not None:
ignition_off = round((ignition_off - start_time) / 1e9, 2)
print("Ignition off at", ignition_off)
print("Route duration", datetime.timedelta(seconds=duration))
+120
View File
@@ -0,0 +1,120 @@
#!/usr/bin/env python3
'''
System tools like top/htop can only show current cpu usage values, so I write this script to do statistics jobs.
Features:
Use psutil library to sample cpu usage(avergage for all cores) of openpilot processes, at a rate of 5 samples/sec.
Do cpu usage statistics periodically, 5 seconds as a cycle.
Calculate the average cpu usage within this cycle.
Calculate minumium/maximum/accumulated_average cpu usage as long term inspections.
Monitor multiple processes simuteneously.
Sample usage:
root@localhost:/data/openpilot$ python openpilot/tools/scripts/cpu_usage_stat.py pandad,ubloxd
('Add monitored proc:', './pandad')
('Add monitored proc:', 'python locationd/ubloxd.py')
pandad: 1.96%, min: 1.96%, max: 1.96%, acc: 1.96%
ubloxd.py: 0.39%, min: 0.39%, max: 0.39%, acc: 0.39%
'''
import psutil
import time
import os
import sys
import numpy as np
import argparse
import re
from collections import defaultdict
from openpilot.system.manager.process_config import managed_processes
# Do statistics every 5 seconds
PRINT_INTERVAL = 5
SLEEP_INTERVAL = 0.2
monitored_proc_names = [
# android procs
'SurfaceFlinger', 'sensors.qcom'
] + list(managed_processes.keys())
cpu_time_names = ['user', 'system', 'children_user', 'children_system']
def get_arg_parser():
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("proc_names", nargs="?", default='',
help="Process names to be monitored, comma separated")
parser.add_argument("--list_all", action='store_true',
help="Show all running processes' cmdline")
parser.add_argument("--detailed_times", action='store_true',
help="show cpu time details (split by user, system, child user, child system)")
return parser
if __name__ == "__main__":
args = get_arg_parser().parse_args(sys.argv[1:])
if args.list_all:
for p in psutil.process_iter():
print('cmdline', p.cmdline(), 'name', p.name())
sys.exit(0)
if len(args.proc_names) > 0:
monitored_proc_names = args.proc_names.split(',')
monitored_procs = []
stats = {}
for p in psutil.process_iter():
if p == psutil.Process():
continue
matched = any(l for l in p.cmdline() if any(pn for pn in monitored_proc_names if re.match(fr'.*{pn}.*', l, re.M | re.I)))
if matched:
k = ' '.join(p.cmdline())
print('Add monitored proc:', k)
stats[k] = {'cpu_samples': defaultdict(list), 'min': defaultdict(lambda: None), 'max': defaultdict(lambda: None),
'avg': defaultdict(float), 'last_cpu_times': None, 'last_sys_time': None}
stats[k]['last_sys_time'] = time.monotonic()
stats[k]['last_cpu_times'] = p.cpu_times()
monitored_procs.append(p)
i = 0
interval_int = int(PRINT_INTERVAL / SLEEP_INTERVAL)
while True:
for p in monitored_procs:
k = ' '.join(p.cmdline())
cur_sys_time = time.monotonic()
cur_cpu_times = p.cpu_times()
cpu_times = np.subtract(cur_cpu_times, stats[k]['last_cpu_times']) / (cur_sys_time - stats[k]['last_sys_time'])
stats[k]['last_sys_time'] = cur_sys_time
stats[k]['last_cpu_times'] = cur_cpu_times
cpu_percent = 0
for num, name in enumerate(cpu_time_names):
stats[k]['cpu_samples'][name].append(cpu_times[num])
cpu_percent += cpu_times[num]
stats[k]['cpu_samples']['total'].append(cpu_percent)
time.sleep(SLEEP_INTERVAL)
i += 1
if i % interval_int == 0:
l = []
for k, stat in stats.items():
if len(stat['cpu_samples']) <= 0:
continue
for name, samples in stat['cpu_samples'].items():
samples = np.array(samples)
avg = samples.mean()
c = samples.size
min_cpu = np.amin(samples)
max_cpu = np.amax(samples)
if stat['min'][name] is None or min_cpu < stat['min'][name]:
stat['min'][name] = min_cpu
if stat['max'][name] is None or max_cpu > stat['max'][name]:
stat['max'][name] = max_cpu
stat['avg'][name] = (stat['avg'][name] * (i - c) + avg * c) / (i)
stat['cpu_samples'][name] = []
msg = f"avg: {stat['avg']['total']:.2%}, min: {stat['min']['total']:.2%}, max: {stat['max']['total']:.2%} {os.path.basename(k)}"
if args.detailed_times:
for stat_type in ['avg', 'min', 'max']:
msg += f"\n {stat_type}: {[(name + ':' + str(round(stat[stat_type][name] * 100, 2))) for name in cpu_time_names]}"
l.append((os.path.basename(k), stat['avg']['total'], msg))
l.sort(key=lambda x: -x[1])
for x in l:
print(x[2])
print('avg sum: {:.2%} over {} samples {} seconds\n'.format(
sum(stat['avg']['total'] for k, stat in stats.items()), i, i * SLEEP_INTERVAL
))
+128
View File
@@ -0,0 +1,128 @@
#!/usr/bin/env python3
import time
import random
from openpilot.cereal import log
from opendbc.car.structs import car
import openpilot.cereal.messaging as messaging
from opendbc.car.honda.interface import CarInterface
from openpilot.common.realtime import DT_CTRL
from openpilot.selfdrive.selfdrived.events import ET, Events
from openpilot.selfdrive.selfdrived.alertmanager import AlertManager
from openpilot.system.manager.process_config import managed_processes
EventName = log.OnroadEvent.EventName
def randperc() -> float:
return 100. * random.random()
def cycle_alerts(duration=200, is_metric=False):
# all alerts
#alerts = list(EVENTS.keys())
# this plays each type of audible alert
alerts = [
(EventName.buttonEnable, ET.ENABLE),
(EventName.buttonCancel, ET.USER_DISABLE),
(EventName.wrongGear, ET.NO_ENTRY),
(EventName.locationdTemporaryError, ET.SOFT_DISABLE),
(EventName.paramsdTemporaryError, ET.SOFT_DISABLE),
(EventName.accFaulted, ET.IMMEDIATE_DISABLE),
# DM sequence
(EventName.driverDistracted1, ET.WARNING),
(EventName.driverDistracted2, ET.WARNING),
(EventName.driverDistracted3, ET.WARNING),
]
# debug alerts
alerts = [
#(EventName.highCpuUsage, ET.NO_ENTRY),
#(EventName.lowMemory, ET.PERMANENT),
#(EventName.overheat, ET.PERMANENT),
#(EventName.outOfSpace, ET.PERMANENT),
#(EventName.modeldLagging, ET.PERMANENT),
#(EventName.processNotRunning, ET.NO_ENTRY),
#(EventName.commIssue, ET.NO_ENTRY),
#(EventName.calibrationInvalid, ET.PERMANENT),
(EventName.cameraMalfunction, ET.PERMANENT),
(EventName.cameraFrameRate, ET.PERMANENT),
]
cameras = ['roadCameraState', 'wideRoadCameraState', 'driverCameraState']
CS = car.CarState.new_message()
CP = CarInterface.get_non_essential_params("HONDA_CIVIC")
sm = messaging.SubMaster(['deviceState', 'pandaStates', 'roadCameraState', 'modelV2', 'liveCalibration',
'driverMonitoringState', 'longitudinalPlan', 'livePose',
'managerState'] + cameras)
pm = messaging.PubMaster(['selfdriveState', 'pandaStates', 'deviceState'])
events = Events()
AM = AlertManager()
frame = 0
while True:
for alert, et in alerts:
events.clear()
events.add(alert)
sm['deviceState'].freeSpacePercent = randperc()
sm['deviceState'].memoryUsagePercent = int(randperc())
sm['deviceState'].cpuTempC = [randperc() for _ in range(3)]
sm['deviceState'].gpuTempC = [randperc() for _ in range(3)]
sm['deviceState'].cpuUsagePercent = [int(randperc()) for _ in range(8)]
sm['modelV2'].frameDropPerc = randperc()
if random.random() > 0.25:
sm['modelV2'].velocity.x = [random.random(), ]
if random.random() > 0.25:
CS.vEgo = random.random()
procs = [p.get_process_state_msg() for p in managed_processes.values()]
random.shuffle(procs)
for i in range(random.randint(0, 10)):
procs[i].shouldBeRunning = True
sm['managerState'].processes = procs
sm['liveCalibration'].rpyCalib = [-1 * random.random() for _ in range(random.randint(0, 3))]
for s in sm.data.keys():
prob = 0.3 if s in cameras else 0.08
sm.alive[s] = random.random() > prob
sm.valid[s] = random.random() > prob
sm.freq_ok[s] = random.random() > prob
a = events.create_alerts([et, ], [CP, CS, sm, is_metric, 0])
AM.add_many(frame, a)
alert = AM.process_alerts(frame, [])
print(alert)
for _ in range(duration):
dat = messaging.new_message('selfdriveState')
dat.selfdriveState.enabled = False
if alert:
dat.selfdriveState.alertText1 = alert.alert_text_1
dat.selfdriveState.alertText2 = alert.alert_text_2
dat.selfdriveState.alertSize = alert.alert_size
dat.selfdriveState.alertStatus = alert.alert_status
dat.selfdriveState.alertType = alert.alert_type
dat.selfdriveState.alertSound = alert.audible_alert
pm.send('selfdriveState', dat)
dat = messaging.new_message('deviceState')
dat.deviceState.started = True
pm.send('deviceState', dat)
dat = messaging.new_message('pandaStates', 1)
dat.pandaStates[0].ignitionLine = True
dat.pandaStates[0].pandaType = log.PandaState.PandaType.uno
pm.send('pandaStates', dat)
frame += 1
time.sleep(DT_CTRL)
if __name__ == '__main__':
cycle_alerts()
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
import argparse
from opendbc.car import uds
from openpilot.tools.lib.live_logreader import live_logreader
from openpilot.tools.lib.logreader import LogReader, ReadMode
def main(route: str | None, addrs: list[int], rxoffset: int | None):
"""
TODO:
- highlight TX vs RX clearly
- disambiguate sendcan and can (useful to know if something sent on sendcan made it to the bus on can->128)
- print as fixed width table, easier to read
"""
if route is None:
lr = live_logreader()
else:
lr = LogReader(route, default_mode=ReadMode.RLOG, sort_by_time=True)
start_mono_time = None
prev_mono_time = 0
# include rx addresses
addrs = addrs + [uds.get_rx_addr_for_tx_addr(addr, rxoffset) for addr in addrs]
for msg in lr:
if msg.which() == 'can':
if start_mono_time is None:
start_mono_time = msg.logMonoTime
if msg.which() in ("can", 'sendcan'):
for can in getattr(msg, msg.which()):
if can.address in addrs or not len(addrs):
if msg.logMonoTime != prev_mono_time:
print()
prev_mono_time = msg.logMonoTime
print(f"{msg.which():>7}: rxaddr={can.address}, bus={str(can.src) + ',':<4} {round((msg.logMonoTime - start_mono_time) * 1e-6)} ms, " +
f"0x{can.dat.hex()}, {can.dat}, {len(can.dat)=}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='View back and forth ISO-TP communication between various ECUs given an address')
parser.add_argument('route', nargs='?', help='Route name, live if not specified')
parser.add_argument('--addrs', nargs='*', default=[], help='List of tx address to view (0x7e0 for engine)')
parser.add_argument('--rxoffset', default='0x8')
args = parser.parse_args()
addrs = [int(addr, base=16) if addr.startswith('0x') else int(addr) for addr in args.addrs]
rxoffset = int(args.rxoffset, base=16) if args.rxoffset else None
main(args.route, addrs, rxoffset)
+93
View File
@@ -0,0 +1,93 @@
#!/usr/bin/env python3
import argparse
import shlex
import subprocess
import threading
import time
from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer
from openpilot.common.basedir import BASEDIR
def build_rsync_cmd(args) -> list[str]:
ssh = [
"ssh",
"-o", "ControlMaster=auto",
"-o", f"ControlPath=/tmp/devsync-{args.ip}.ctl",
"-o", "ControlPersist=10m",
"-o", "StrictHostKeyChecking=accept-new",
]
if args.identity:
ssh += ["-i", args.identity]
return [
"rsync", "-az",
"--files-from=-", "--from0",
"-e", " ".join(shlex.quote(p) for p in ssh),
"--out-format=%n",
BASEDIR + "/", f"comma@{args.ip}:{args.remote}/",
]
def git_tracked_files() -> bytes:
return subprocess.check_output(
["git", "-C", BASEDIR, "ls-files", "--recurse-submodules", "-z"]
)
class Handler(FileSystemEventHandler):
def __init__(self, sync_fn):
self.dirty = threading.Event()
self.sync_fn = sync_fn
def on_any_event(self, event):
if not event.is_directory:
self.dirty.set()
def run(self):
while True:
time.sleep(1)
if self.dirty.is_set():
self.dirty.clear()
self.sync_fn()
def main():
p = argparse.ArgumentParser()
p.add_argument("ip", help="device IP / hostname")
p.add_argument("--remote", default="/data/openpilot", help="remote path on device")
p.add_argument("-i", "--identity", default=None, help="ssh identity file")
args = p.parse_args()
print(f"[devsync] watching {BASEDIR}")
print(f"[devsync] target comma@{args.ip}:{args.remote}")
def run_sync():
file_list = git_tracked_files()
cmd = build_rsync_cmd(args)
t0 = time.monotonic()
r = subprocess.run(cmd, input=file_list, capture_output=True)
dt = time.monotonic() - t0
if r.returncode:
print(f"[devsync] ERR rc={r.returncode} in {dt:.2f}s")
return
files = [ln for ln in r.stdout.decode().splitlines() if ln.strip()]
msg = f"{len(files)} files: {', '.join(files)}" if files else "no changes"
print(f"[devsync] {dt:.2f}s · {msg}")
run_sync()
handler = Handler(run_sync)
obs = Observer()
obs.schedule(handler, BASEDIR, recursive=True)
obs.start()
handler.run()
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\n[devsync] stopping")
+74
View File
@@ -0,0 +1,74 @@
#!/usr/bin/env python3
import sys
import argparse
import json
import codecs
from openpilot.cereal import log
from openpilot.cereal.services import SERVICE_LIST
from openpilot.tools.lib.live_logreader import raw_live_logreader
codecs.register_error("strict", codecs.backslashreplace_errors)
def hexdump(msg):
m = str.upper(msg.hex())
m = [m[i:i+2] for i in range(0,len(m),2)]
m = [m[i:i+16] for i in range(0,len(m),16)]
for row,dump in enumerate(m):
addr = '%08X:' % (row*16)
raw = ' '.join(dump[:8]) + ' ' + ' '.join(dump[8:])
space = ' ' * (48 - len(raw))
asci = ''.join(chr(int(x,16)) if 0x20 <= int(x,16) <= 0x7E else '.' for x in dump)
print(f'{addr} {raw} {space} {asci}')
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Dump communication sockets. See openpilot/cereal/services.py for a complete list of available sockets.')
parser.add_argument('--pipe', action='store_true')
parser.add_argument('--raw', action='store_true')
parser.add_argument('--json', action='store_true')
parser.add_argument('--dump-json', action='store_true')
parser.add_argument('--no-print', action='store_true')
parser.add_argument('--addr', default='127.0.0.1')
parser.add_argument('--values', help='values to monitor (instead of entire event)')
parser.add_argument("socket", type=str, nargs='*', default=list(SERVICE_LIST.keys()), help="socket names to dump. defaults to all services defined in cereal")
args = parser.parse_args()
lr = raw_live_logreader(args.socket, args.addr)
values = None
if args.values:
values = [s.strip().split(".") for s in args.values.split(",")]
for msg in lr:
with log.Event.from_bytes(msg) as evt:
if not args.no_print:
if args.pipe:
sys.stdout.write(str(msg))
sys.stdout.flush()
elif args.raw:
hexdump(msg)
elif args.json:
print(json.loads(msg))
elif args.dump_json:
print(json.dumps(evt.to_dict()))
elif values:
print(f"logMonotime = {evt.logMonoTime}")
for value in values:
if hasattr(evt, value[0]):
item = evt
for key in value:
item = getattr(item, key)
print(f"{'.'.join(value)} = {item}")
print("")
else:
try:
print(evt)
except UnicodeDecodeError:
w = evt.which()
s = f"( logMonoTime {evt.logMonoTime} \n {w} = "
s += str(evt.__getattr__(w))
s += f"\n valid = {evt.valid} )"
print(s)
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env python3
import os
import sys
import wave
import argparse
import numpy as np
from openpilot.tools.lib.logreader import LogReader, ReadMode
def extract_audio(route_or_segment_name, output_file=None, play=False):
lr = LogReader(route_or_segment_name, default_mode=ReadMode.AUTO_INTERACTIVE)
audio_messages = list(lr.filter("rawAudioData"))
if not audio_messages:
print("No rawAudioData messages found in logs")
return
sample_rate = audio_messages[0].sampleRate
audio_chunks = []
total_frames = 0
for msg in audio_messages:
audio_array = np.frombuffer(msg.data, dtype=np.int16)
audio_chunks.append(audio_array)
total_frames += len(audio_array)
full_audio = np.concatenate(audio_chunks)
print(f"Found {total_frames} frames from {len(audio_messages)} audio messages at {sample_rate} Hz")
if output_file:
if write_wav_file(output_file, full_audio, sample_rate):
print(f"Audio written to {output_file}")
else:
print("Audio extraction canceled.")
if play:
play_audio(full_audio, sample_rate)
def write_wav_file(filename, audio_data, sample_rate):
if os.path.exists(filename):
if input(f"File '{filename}' exists. Overwrite? (y/N): ").lower() not in ['y', 'yes']:
return False
with wave.open(filename, 'wb') as wav_file:
wav_file.setnchannels(1) # Mono
wav_file.setsampwidth(2) # 16-bit
wav_file.setframerate(sample_rate)
wav_file.writeframes(audio_data.tobytes())
return True
def play_audio(audio_data, sample_rate):
try:
import sounddevice as sd
print("Playing audio... Press Ctrl+C to stop")
sd.play(audio_data, sample_rate)
sd.wait()
except KeyboardInterrupt:
print("\nPlayback stopped")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Extract audio data from openpilot logs")
parser.add_argument("-o", "--output", help="Output WAV file path")
parser.add_argument("--play", action="store_true", help="Play audio with sounddevice")
parser.add_argument("route_or_segment_name", nargs='?', help="The route or segment name")
if len(sys.argv) == 1:
parser.print_help()
sys.exit()
args = parser.parse_args()
output_file = args.output
if not args.output and not args.play:
output_file = "extracted_audio.wav"
extract_audio(args.route_or_segment_name.strip(), output_file, args.play)
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
import argparse
import json
import openpilot.cereal.messaging as messaging
from openpilot.tools.lib.logreader import LogReader
LEVELS = {
"DEBUG": 10,
"INFO": 20,
"WARNING": 30,
"ERROR": 40,
"CRITICAL": 50,
}
OPERATING_SYSTEM_LOG_SOURCE = {
0: "MAIN",
1: "RADIO",
2: "EVENTS",
3: "SYSTEM",
4: "CRASH",
5: "KERNEL",
}
def print_logmessage(t, msg, min_level):
try:
log = json.loads(msg)
if log['levelnum'] >= min_level:
print(f"[{t / 1e9:.6f}] {log['filename']}:{log.get('lineno', '')} - {log.get('funcname', '')}: {log['msg']}")
if 'exc_info' in log:
print(log['exc_info'])
except json.decoder.JSONDecodeError:
print(f"[{t / 1e9:.6f}] decode error: {msg}")
def print_operating_system_log(t, msg):
source = msg.tag or OPERATING_SYSTEM_LOG_SOURCE.get(msg.id, "SYSTEM")
try:
m = json.loads(msg.message)['MESSAGE']
except Exception:
m = msg.message
print(f"[{t / 1e9:.6f}] {source} {msg.pid} {msg.tag} - {m}")
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument('--absolute', action='store_true')
parser.add_argument('--level', default='DEBUG')
parser.add_argument('--addr', default='127.0.0.1')
parser.add_argument("route", type=str, nargs='*', help="route name + segment number for offline usage")
args = parser.parse_args()
min_level = LEVELS[args.level]
if args.route:
st = None if not args.absolute else 0
for route in args.route:
lr = LogReader(route, sort_by_time=True)
for m in lr:
if st is None:
st = m.logMonoTime
if m.which() == 'logMessage':
print_logmessage(m.logMonoTime-st, m.logMessage, min_level)
elif m.which() == 'errorLogMessage':
print_logmessage(m.logMonoTime-st, m.errorLogMessage, min_level)
elif m.which() == 'operatingSystemLog':
print_operating_system_log(m.logMonoTime-st, m.operatingSystemLog)
else:
sm = messaging.SubMaster(['logMessage', 'operatingSystemLog'], addr=args.addr)
while True:
sm.update()
if sm.updated['logMessage']:
print_logmessage(sm.logMonoTime['logMessage'], sm['logMessage'], min_level)
if sm.updated['operatingSystemLog']:
print_operating_system_log(sm.logMonoTime['operatingSystemLog'], sm['operatingSystemLog'])
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env python3
import sys
from openpilot.tools.lib.logreader import LogReader, ReadMode
def get_fingerprint(lr):
# TODO: make this a nice tool for car ports. should also work with qlogs for FW
fw = None
vin = None
msgs = {}
for msg in lr:
if msg.which() == 'carParams':
fw = msg.carParams.carFw
vin = msg.carParams.carVin
elif msg.which() == 'can':
for c in msg.can:
# read also msgs sent by EON on CAN bus 0x80 and filter out the
# addr with more than 11 bits
if c.src % 0x80 == 0 and c.address < 0x800 and c.address not in (0x7df, 0x7e0, 0x7e8):
msgs[c.address] = len(c.dat)
# show CAN fingerprint
fingerprint = ', '.join(f"{v[0]}: {v[1]}" for v in sorted(msgs.items()))
print(f"\nfound {len(msgs)} messages. CAN fingerprint:\n")
print(fingerprint)
# TODO: also print the fw fingerprint merged with the existing ones
# show FW fingerprint
if fw:
print("\nFW fingerprint:\n")
for f in fw:
print(f" (Ecu.{f.ecu}, {hex(f.address)}, {None if f.subAddress == 0 else f.subAddress}): [")
print(f" {f.fwVersion},")
print(" ],")
print()
print(f"VIN: {vin}")
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: ./fingerprint_from_route.py <route>")
sys.exit(1)
lr = LogReader(sys.argv[1], ReadMode.QLOG)
get_fingerprint(lr)
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env python3
import random
from collections import defaultdict
from tqdm import tqdm
from opendbc.car.fw_versions import match_fw_to_car_fuzzy
from opendbc.car.toyota.values import FW_VERSIONS as TOYOTA_FW_VERSIONS
from opendbc.car.honda.values import FW_VERSIONS as HONDA_FW_VERSIONS
from opendbc.car.hyundai.values import FW_VERSIONS as HYUNDAI_FW_VERSIONS
from opendbc.car.volkswagen.values import FW_VERSIONS as VW_FW_VERSIONS
FWS = {}
FWS.update(TOYOTA_FW_VERSIONS)
FWS.update(HONDA_FW_VERSIONS)
FWS.update(HYUNDAI_FW_VERSIONS)
FWS.update(VW_FW_VERSIONS)
if __name__ == "__main__":
total = 0
match = 0
wrong_match = 0
confusions = defaultdict(set)
for _ in tqdm(range(1000)):
for candidate, fws in FWS.items():
fw_dict = {}
for (_, addr, subaddr), fw_list in fws.items():
fw_dict[(addr, subaddr)] = [random.choice(fw_list)]
matches = match_fw_to_car_fuzzy(fw_dict, log=False, exclude=candidate)
total += 1
if len(matches) == 1:
if list(matches)[0] == candidate:
match += 1
else:
confusions[candidate] |= matches
wrong_match += 1
print()
for candidate, wrong_matches in sorted(confusions.items()):
print(candidate, wrong_matches)
print()
print(f"Total fuzz cases: {total}")
print(f"Correct matches: {match}")
print(f"Wrong matches: {wrong_match}")
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env python3
# simple script to get a vehicle fingerprint.
# Instructions:
# - connect to a Panda
# - run openpilot/selfdrive/pandad/pandad
# - launching this script
# Note: it's very important that the car is in stock mode, in order to collect a complete fingerprint
# - since some messages are published at low frequency, keep this script running for at least 30s,
# until all messages are received at least once
import openpilot.cereal.messaging as messaging
logcan = messaging.sub_sock('can')
msgs = {}
while True:
lc = messaging.recv_sock(logcan, True)
if lc is None:
continue
for c in lc.can:
# read also msgs sent by EON on CAN bus 0x80 and filter out the
# addr with more than 11 bits
if c.src % 0x80 == 0 and c.address < 0x800 and c.address not in (0x7df, 0x7e0, 0x7e8):
msgs[c.address] = len(c.dat)
fingerprint = ', '.join(f"{v[0]}: {v[1]}" for v in sorted(msgs.items()))
print(f"number of messages {len(msgs)}:")
print(f"fingerprint {fingerprint}")
+106
View File
@@ -0,0 +1,106 @@
#!/usr/bin/env python3
import argparse
import numpy as np
import capnp
from collections import defaultdict
from openpilot.cereal.messaging import SubMaster
def cputime_total(ct):
return ct.user + ct.nice + ct.system + ct.idle + ct.iowait + ct.irq + ct.softirq
def cputime_busy(ct):
return ct.user + ct.nice + ct.system + ct.irq + ct.softirq
def proc_cputime_total(ct):
return ct.cpuUser + ct.cpuSystem + ct.cpuChildrenUser + ct.cpuChildrenSystem
def proc_name(proc):
name = proc.name
if len(proc.cmdline):
name = proc.cmdline[0]
if len(proc.exe):
name = proc.exe + " - " + name
return name
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument('--mem', action='store_true')
parser.add_argument('--cpu', action='store_true')
args = parser.parse_args()
sm = SubMaster(['deviceState', 'procLog'])
last_temp = 0.0
last_mem = 0.0
total_times = [0.]*8
busy_times = [0.]*8
prev_proclog: capnp._DynamicStructReader | None = None
prev_proclog_t: int | None = None
while True:
sm.update()
if sm.updated['deviceState']:
t = sm['deviceState']
last_temp = np.mean(t.cpuTempC)
last_mem = t.memoryUsagePercent
if sm.updated['procLog']:
m = sm['procLog']
cores = [0.]*8
total_times_new = [0.]*8
busy_times_new = [0.]*8
for c in m.cpuTimes:
n = c.cpuNum
total_times_new[n] = cputime_total(c)
busy_times_new[n] = cputime_busy(c)
for n in range(8):
t_busy = busy_times_new[n] - busy_times[n]
t_total = total_times_new[n] - total_times[n]
cores[n] = t_busy / t_total
total_times = total_times_new[:]
busy_times = busy_times_new[:]
print(f"CPU {100.0 * np.mean(cores):.2f}% - RAM: {last_mem:.2f}% - Temp {last_temp:.2f}C")
if args.cpu and prev_proclog is not None and prev_proclog_t is not None:
procs: dict[str, float] = defaultdict(float)
dt = (sm.logMonoTime['procLog'] - prev_proclog_t) / 1e9
for proc in m.procs:
try:
name = proc_name(proc)
prev_proc = [p for p in prev_proclog.procs if proc.pid == p.pid][0]
cpu_time = proc_cputime_total(proc) - proc_cputime_total(prev_proc)
cpu_usage = cpu_time / dt * 100.
procs[name] += cpu_usage
except IndexError:
pass
print("Top CPU usage:")
for k, v in sorted(procs.items(), key=lambda item: item[1], reverse=True)[:10]:
print(f"{k.rjust(70)} {v:.2f} %")
print()
if args.mem:
mems = {}
for proc in m.procs:
name = proc_name(proc)
mems[name] = float(proc.memRss) / 1e6
print("Top memory usage:")
for k, v in sorted(mems.items(), key=lambda item: item[1], reverse=True)[:10]:
print(f"{k.rjust(70)} {v:.2f} MB")
print()
prev_proclog = m
prev_proclog_t = sm.logMonoTime['procLog']
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env python3
import argparse
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
from openpilot.selfdrive.test.mem_usage import DEMO_ROUTE, print_report
from openpilot.tools.lib.logreader import LogReader
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Analyze memory usage from route logs")
parser.add_argument("route", nargs="?", default=None, help="route ID or local rlog path")
parser.add_argument("--demo", action="store_true", help=f"use demo route ({DEMO_ROUTE})")
args = parser.parse_args()
if args.demo:
route = DEMO_ROUTE
elif args.route:
route = args.route
else:
parser.error("provide a route or use --demo")
print(f"Reading logs from: {route}")
proc_logs = []
device_states = []
for msg in LogReader(route):
if msg.which() == 'procLog':
proc_logs.append(msg)
elif msg.which() == 'deviceState':
device_states.append(msg)
print_report(proc_logs, device_states)
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env python3
from opendbc.car.values import BRANDS
for brand in BRANDS:
all_flags = set()
for platform in brand:
if platform.config.flags != 0:
all_flags |= set(platform.config.flags)
if len(all_flags):
print(brand.__module__.split('.')[-2].upper() + ':')
for flag in sorted(all_flags):
print(f' {flag.name:<24}:', {platform.name for platform in brand.with_flags(flag)})
print()
@@ -0,0 +1 @@
clpeak/
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
cd $DIR
if [ ! -d "$DIR/clpeak" ]; then
git clone https://github.com/krrishnarraj/clpeak.git
cd clpeak
git fetch
git checkout ec2d3e70e1abc7738b81f9277c7af79d89b2133b
git reset --hard origin/master
git submodule update --init --recursive --remote
git apply ../run_continuously.patch
fi
cd clpeak
mkdir build || true
cd build
cmake ..
cmake --build .
@@ -0,0 +1,39 @@
diff --git a/src/logger.cpp b/src/logger.cpp
index a63c6dd..a1d9860 100644
--- a/src/logger.cpp
+++ b/src/logger.cpp
@@ -24,34 +24,22 @@ logger::~logger()
void logger::print(string str)
{
- cout << str;
- cout.flush();
}
void logger::print(double val)
{
- cout << setprecision(2) << fixed;
- cout << val;
- cout.flush();
}
void logger::print(float val)
{
- cout << setprecision(2) << fixed;
- cout << val;
- cout.flush();
}
void logger::print(int val)
{
- cout << val;
- cout.flush();
}
void logger::print(unsigned int val)
{
- cout << val;
- cout.flush();
}
void logger::xmlOpenTag(string tag)
@@ -0,0 +1,13 @@
diff --git a/src/clpeak.cpp b/src/clpeak.cpp
index 8cb192b..b6fe6f5 100644
--- a/src/clpeak.cpp
+++ b/src/clpeak.cpp
@@ -47,7 +47,7 @@ int clPeak::runAll()
log->xmlOpenTag("clpeak");
log->xmlAppendAttribs("os", OS_NAME);
- for (size_t p = 0; p < platforms.size(); p++)
+ for (size_t p = 0; p < platforms.size(); (p+1 % platforms.size()))
{
if (forcePlatform && (p != specifiedPlatform))
continue;
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
set -e
cd /sys/kernel/tracing
echo 1 > tracing_on
echo boot > trace_clock
echo 1000 > buffer_size_kb
# /sys/kernel/tracing/available_events
echo 0 > events/enable
#echo 1 > events/irq/enable
#echo 1 > events/sched/enable
#echo 1 > events/kgsl/enable
#echo 1 > events/camera/enable
echo 1 > events/workqueue/enable
echo > trace
sleep 2
echo 0 > tracing_on
cp trace /tmp/trace
chown comma: /tmp/trace
echo /tmp/trace
@@ -0,0 +1,2 @@
palanteer/
viewer
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
set -e
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
cd $DIR
if [ ! -d palanteer ]; then
git clone https://github.com/dfeneyrou/palanteer
pip install wheel
sudo apt install libunwind-dev libdw-dev
fi
cd palanteer
git pull
mkdir -p build
cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j$(nproc)
pip install --force-reinstall python/dist/palanteer*.whl
cp bin/palanteer $DIR/viewer
@@ -0,0 +1,7 @@
trace_*
tracebox
trace_processor
perfetto/
configs/
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
if [ ! -d perfetto ]; then
git clone https://android.googlesource.com/platform/external/perfetto/
fi
cd perfetto
tools/install-build-deps --linux-arm
tools/gn gen --args='is_debug=false target_os="linux" target_cpu="arm64"' out/linux
tools/ninja -C out/linux tracebox traced traced_probes perfetto
+6
View File
@@ -0,0 +1,6 @@
#!/usr/bin/env bash
DEST=tici:/data/openpilot/selfdrive/debug/profiling/perfetto
scp -r perfetto/out/linux/tracebox $DEST
scp -r perfetto/test/configs $DEST
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)"
cd $DIR
OUT=trace_
sudo ./tracebox -o $OUT --txt -c configs/scheduling.cfg
sudo chown $USER:$USER $OUT
+6
View File
@@ -0,0 +1,6 @@
#!/usr/bin/env bash
curl -LO https://get.perfetto.dev/trace_processor
chmod +x ./trace_processor
./trace_processor --httpd
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
DEST=tici:/data/openpilot/selfdrive/debug/profiling/perfetto
scp tici:/data/openpilot/selfdrive/debug/profiling/perfetto/trace_* .
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -e
cd "$(dirname "$0")"
# find process with name passed in (excluding this process)
for PID in $(pgrep -f $1); do
if [ "$PID" != "$$" ]; then
ps -p $PID -o args
TRACE_PID=$PID
break
fi
done
if [ -z "$TRACE_PID" ]; then
echo "could not find PID for $1"
exit 1
fi
sudo env PATH=$PATH py-spy record -d 5 -o /tmp/perf$TRACE_PID.svg -p $TRACE_PID &&
google-chrome /tmp/perf$TRACE_PID.svg
@@ -0,0 +1 @@
SnapdragonProfiler/
@@ -0,0 +1,13 @@
snapdragon profiler
--------
* download from https://developer.qualcomm.com/software/snapdragon-profiler/tools-archive (need a qc developer account)
* choose v2021.5 (verified working with 24.04 dev environment)
* unzip to openpilot/selfdrive/debug/profiling/snapdragon/SnapdragonProfiler
* run ```./setup-profiler.sh```
* run ```./setup-agnos.sh```
* run ```openpilot/selfdrive/debug/adb.sh``` on device
* run the ```adb connect xxx``` command that was given to you on local pc
* cd to SnapdragonProfiler and run ```./run_sdp.sh```
* connect to device -> choose device you just setup
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
# TODO: there's probably a better way to do this
cd SnapdragonProfiler/service
mv android real_android
ln -s agl/ android
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
# install depends
sudo apt update
sudo apt-get install libc++1 libc++abi1 default-jre android-tools-adb gtk-sharp2
# setup mono
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF
sudo apt install apt-transport-https ca-certificates
echo "deb https://download.mono-project.com/repo/ubuntu stable-xenial main" | sudo tee /etc/apt/sources.list.d/mono-official-stable.list
sudo apt update
sudo apt-get install -y mono-complete
echo "Setup successful, you should now be able to run the profiler with cd SnapdragonProfiler and ./run_sdp.sh"
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -e
RUBYOPT="-W0" irqtop -d1 -R
+78
View File
@@ -0,0 +1,78 @@
#!/usr/bin/env python3
import argparse
import zstandard as zstd
from collections import defaultdict
import matplotlib.pyplot as plt
from openpilot.cereal.services import SERVICE_LIST
from openpilot.common.utils import LOG_COMPRESSION_LEVEL
from openpilot.tools.lib.logreader import LogReader
from tqdm import tqdm
MIN_SIZE = 0.5 # Percent size of total to show as separate entry
def make_pie(msgs, typ):
msgs_by_type = defaultdict(list)
for m in msgs:
msgs_by_type[m.which()].append(m.as_builder().to_bytes())
total = len(zstd.compress(b"".join([m.as_builder().to_bytes() for m in msgs]), LOG_COMPRESSION_LEVEL))
uncompressed_total = len(b"".join([m.as_builder().to_bytes() for m in msgs]))
length_by_type = {k: len(b"".join(v)) for k, v in msgs_by_type.items()}
# calculate compressed size by calculating diff when removed from the segment
compressed_length_by_type = {}
for k in tqdm(msgs_by_type.keys(), desc="Compressing"):
compressed_length_by_type[k] = total - len(zstd.compress(b"".join([m.as_builder().to_bytes() for m in msgs if m.which() != k]), LOG_COMPRESSION_LEVEL))
sizes = sorted(compressed_length_by_type.items(), key=lambda kv: kv[1])
print("name - comp. size (uncomp. size)")
for (name, sz) in sizes:
print(f"{name:<22} - {sz / 1024:.2f} kB ({length_by_type[name] / 1024:.2f} kB)")
print()
print(f"{typ} - Real total {total / 1024:.2f} kB")
print(f"{typ} - Breakdown total {sum(compressed_length_by_type.values()) / 1024:.2f} kB")
print(f"{typ} - Uncompressed total {uncompressed_total / 1024 / 1024:.2f} MB")
sizes_large = [(k, sz) for (k, sz) in sizes if sz >= total * MIN_SIZE / 100]
sizes_large += [('other', sum(sz for (_, sz) in sizes if sz < total * MIN_SIZE / 100))]
labels, sizes = zip(*sizes_large, strict=True)
plt.figure()
plt.title(f"{typ}")
plt.pie(sizes, labels=labels, autopct='%1.1f%%')
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='View log size breakdown by message type')
parser.add_argument('route', help='route to use')
parser.add_argument('--as-qlog', action='store_true', help='decimate rlog using latest decimation factors')
args = parser.parse_args()
msgs = list(LogReader(args.route))
if args.as_qlog:
new_msgs = []
msg_cnts: dict[str, int] = defaultdict(int)
for msg in msgs:
msg_which = msg.which()
if msg.which() in ("initData", "sentinel"):
new_msgs.append(msg)
continue
if msg_which not in SERVICE_LIST:
continue
decimation = SERVICE_LIST[msg_which].decimation
if decimation is not None and msg_cnts[msg_which] % decimation == 0:
new_msgs.append(msg)
msg_cnts[msg_which] += 1
msgs = new_msgs
make_pie(msgs, 'qlog')
plt.show()
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env python3
import argparse
from openpilot.selfdrive.test.process_replay.process_replay import CONFIGS, replay_process
from openpilot.selfdrive.test.process_replay.test_processes import EXCLUDED_PROCS
from openpilot.tools.lib.logreader import LogReader, save_log
ALLOW_PROCS = {c.proc_name for c in CONFIGS}
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Run process on route and create new logs",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("route", help="The route name to use")
parser.add_argument("--fingerprint", help="The fingerprint to use")
parser.add_argument("--whitelist-procs", nargs='*', default=ALLOW_PROCS, help="Whitelist given processes (e.g. controlsd)")
parser.add_argument("--blacklist-procs", nargs='*', default=EXCLUDED_PROCS, help="Blacklist given processes (e.g. controlsd)")
args = parser.parse_args()
allowed_procs = set(args.whitelist_procs) - set(args.blacklist_procs)
cfgs = [c for c in CONFIGS if c.proc_name in allowed_procs]
inputs = list(LogReader(args.route))
outputs = replay_process(cfgs, inputs, fingerprint=args.fingerprint)
# Remove message generated by the process under test and merge in the new messages
produces = {o.which() for o in outputs}
inputs = [i for i in inputs if i.which() not in produces]
outputs = sorted(inputs + outputs, key=lambda x: x.logMonoTime)
fn = f"{args.route.replace('/', '_')}_{'_'.join(allowed_procs)}.zst"
print(f"Saving log to {fn}")
save_log(fn, outputs)
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
while true; do
if ls /dev/serial/by-id/usb-FTDI_FT230X* 2> /dev/null; then
sudo screen /dev/serial/by-id/usb-FTDI_FT230X* 115200
fi
sleep 0.005
done
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env python3
import sys
from opendbc.car.structs import car
from openpilot.common.params import Params
from openpilot.tools.lib.route import Route
from openpilot.tools.lib.logreader import LogReader
if __name__ == "__main__":
CP = None
if len(sys.argv) > 1:
r = Route(sys.argv[1])
cps = [m for m in LogReader(r.qlog_paths()[0]) if m.which() == 'carParams']
CP = cps[0].carParams.as_builder()
else:
CP = car.CarParams.new_message()
CP.openpilotLongitudinalControl = True
CP.alphaLongitudinalAvailable = False
cp_bytes = CP.to_bytes()
for p in ("CarParams", "CarParamsCache", "CarParamsPersistent"):
Params().put(p, cp_bytes, block=True)
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env python3
import requests
from openpilot.common.params import Params
import sys
if __name__ == "__main__":
if len(sys.argv) < 2:
print(f"{sys.argv[0]} <github username>")
exit(1)
username = sys.argv[1]
keys = requests.get(f"https://github.com/{username}.keys", timeout=10)
if keys.status_code == 200:
params = Params()
params.put_bool("SshEnabled", True, block=True)
params.put("GithubSshKeys", keys.text, block=True)
params.put("GithubUsername", username, block=True)
print("Set up ssh keys successfully")
else:
print("Error getting public keys from github")
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env python3
import os
import sys
import argparse
import re
from openpilot.common.basedir import BASEDIR
from openpilot.tools.lib.auth_config import get_token
from openpilot.tools.lib.api import CommaApi
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="A helper for connecting to devices over the comma prime SSH proxy.\
Adding your SSH key to your SSH config is recommended for more convenient use; see https://docs.comma.ai/how-to/connect-to-comma/.")
parser.add_argument("device", help="device name or dongle id")
parser.add_argument("--host", help="ssh jump server host", default="ssh.comma.ai")
parser.add_argument("--port", help="ssh jump server port", default=22, type=int)
parser.add_argument("--key", help="ssh key", default=os.path.join(BASEDIR, "openpilot/common/hardware/tici/id_rsa"))
parser.add_argument("--debug", help="enable debug output", action="store_true")
args = parser.parse_args()
r = CommaApi(get_token()).get("v1/me/devices")
devices = {x['dongle_id']: x['alias'] for x in r}
if not re.match("[0-9a-zA-Z]{16}", args.device):
user_input = args.device.replace(" ", "").lower()
matches = { k: v for k, v in devices.items() if isinstance(v, str) and user_input in v.replace(" ", "").lower() }
if len(matches) == 1:
dongle_id = list(matches.keys())[0]
else:
print(f"failed to look up dongle id for \"{args.device}\"", file=sys.stderr)
if len(matches) > 1:
print("found multiple matches:", file=sys.stderr)
for k, v in matches.items():
print(f" \"{v}\" ({k})", file=sys.stderr)
exit(1)
else:
dongle_id = args.device
name = dongle_id
if dongle_id in devices:
name = f"{devices[dongle_id]} ({dongle_id})"
print(f"connecting to {name} through {args.host}:{args.port} ...")
command = [
"ssh",
"-i", args.key,
"-o", f"ProxyCommand=ssh -i {args.key} -W %h:%p -p %p %h@{args.host}",
"-p", str(args.port),
]
if args.debug:
command += ["-v"]
command += [
f"comma@comma-{dongle_id}",
]
if args.debug:
print(" ".join([f"'{c}'" if " " in c else c for c in command]))
os.execvp(command[0], command)
+181
View File
@@ -0,0 +1,181 @@
#!/usr/bin/env python3
from collections import defaultdict
import argparse
import os
import traceback
from tqdm import tqdm
from opendbc.car.car_helpers import interface_names
from opendbc.car.fingerprints import MIGRATION
from opendbc.car.fw_versions import VERSIONS, match_fw_to_car
from openpilot.tools.lib.logreader import LogReader, ReadMode
from openpilot.tools.lib.route import SegmentRange
NO_API = "NO_API" in os.environ
SUPPORTED_BRANDS = VERSIONS.keys()
SUPPORTED_CARS = [brand for brand in SUPPORTED_BRANDS for brand in interface_names[brand]]
UNKNOWN_BRAND = "unknown"
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Run FW fingerprint on Qlog of route or list of routes')
parser.add_argument('route', help='Route or file with list of routes')
parser.add_argument('--car', help='Force comparison fingerprint to known car')
args = parser.parse_args()
if os.path.exists(args.route):
routes = list(open(args.route))
else:
routes = [args.route]
mismatches = defaultdict(list)
not_fingerprinted = 0
solved_by_fuzzy = 0
good_exact = 0
wrong_fuzzy = 0
good_fuzzy = 0
dongles = []
for route in tqdm(routes):
sr = SegmentRange(route)
dongle_id = sr.dongle_id
if dongle_id in dongles:
continue
if sr.slice == '' and sr.selector is None:
route += '/0'
lr = LogReader(route, default_mode=ReadMode.QLOG)
try:
dongles.append(dongle_id)
CP = None
for msg in lr:
if msg.which() == "pandaStates":
if msg.pandaStates[0].pandaType in ('unknown', 'whitePanda', 'greyPanda', 'pedal'):
print("wrong panda type")
break
elif msg.which() == "carParams":
CP = msg.carParams
car_fw = [fw for fw in CP.carFw if not fw.logging]
if len(car_fw) == 0:
print("WARNING: no fw")
live_fingerprint = CP.carFingerprint
live_fingerprint = MIGRATION.get(live_fingerprint, live_fingerprint)
if args.car is not None:
live_fingerprint = args.car
if live_fingerprint not in SUPPORTED_CARS:
print("not in supported cars")
break
_, exact_matches = match_fw_to_car(car_fw, CP.carVin, allow_exact=True, allow_fuzzy=False)
_, fuzzy_matches = match_fw_to_car(car_fw, CP.carVin, allow_exact=False, allow_fuzzy=True)
if (len(exact_matches) == 1) and (list(exact_matches)[0] == live_fingerprint):
good_exact += 1
print(f"Correct! Live: {live_fingerprint} - Fuzzy: {fuzzy_matches}")
# Check if fuzzy match was correct
if len(fuzzy_matches) == 1:
if list(fuzzy_matches)[0] != live_fingerprint:
wrong_fuzzy += 1
print("Fuzzy match wrong! Fuzzy:", fuzzy_matches, "Live:", live_fingerprint)
else:
good_fuzzy += 1
break
print("Old style:", live_fingerprint, "Vin", CP.carVin)
print("New style (exact):", exact_matches)
print("New style (fuzzy):", fuzzy_matches)
padding = max([len(fw.brand or UNKNOWN_BRAND) for fw in car_fw] + [0])
for version in sorted(car_fw, key=lambda fw: fw.brand):
subaddr = None if version.subAddress == 0 else hex(version.subAddress)
print(f" Brand: {version.brand or UNKNOWN_BRAND:{padding}}, bus: {version.bus} - " +
f"(Ecu.{version.ecu}, {hex(version.address)}, {subaddr}): [{version.fwVersion}],")
print("Mismatches")
found = False
for brand in SUPPORTED_BRANDS:
car_fws = VERSIONS[brand]
if live_fingerprint in car_fws:
found = True
expected = car_fws[live_fingerprint]
for (_, expected_addr, expected_sub_addr), v in expected.items():
for version in car_fw:
if version.brand != brand and len(version.brand):
continue
sub_addr = None if version.subAddress == 0 else version.subAddress
addr = version.address
if (addr, sub_addr) == (expected_addr, expected_sub_addr):
if version.fwVersion not in v:
print(f"({hex(addr)}, {'None' if sub_addr is None else hex(sub_addr)}) - {version.fwVersion}")
# Add to global list of mismatches
mismatch = (addr, sub_addr, version.fwVersion)
if mismatch not in mismatches[live_fingerprint]:
mismatches[live_fingerprint].append(mismatch)
# No FW versions for this car yet, add them all to mismatch list
if not found:
for version in car_fw:
sub_addr = None if version.subAddress == 0 else version.subAddress
addr = version.address
mismatch = (addr, sub_addr, version.fwVersion)
if mismatch not in mismatches[live_fingerprint]:
mismatches[live_fingerprint].append(mismatch)
print()
not_fingerprinted += 1
if len(fuzzy_matches) == 1:
if list(fuzzy_matches)[0] == live_fingerprint:
solved_by_fuzzy += 1
else:
wrong_fuzzy += 1
print("Fuzzy match wrong! Fuzzy:", fuzzy_matches, "Live:", live_fingerprint)
break
if CP is None:
print("no CarParams in logs")
except Exception:
traceback.print_exc()
except KeyboardInterrupt:
break
print()
# Print FW versions that need to be added separated out by car and address
for car, m in sorted(mismatches.items()):
print(car)
addrs = defaultdict(list)
for (addr, sub_addr, version) in m:
addrs[(addr, sub_addr)].append(version)
for (addr, sub_addr), versions in addrs.items():
print(f" ({hex(addr)}, {'None' if sub_addr is None else hex(sub_addr)}): [")
for v in versions:
print(f" {v},")
print(" ]")
print()
print()
print(f"Number of dongle ids checked: {len(dongles)}")
print(f"Fingerprinted: {good_exact}")
print(f"Not fingerprinted: {not_fingerprinted}")
print(f" of which had a fuzzy match: {solved_by_fuzzy}")
print()
print(f"Correct fuzzy matches: {good_fuzzy}")
print(f"Wrong fuzzy matches: {wrong_fuzzy}")
print()
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env python3
import time
from openpilot.cereal import log, messaging
from opendbc.car.structs import car
from openpilot.common.params import Params
from openpilot.system.manager.process_config import managed_processes
from openpilot.common.hardware import HARDWARE
if __name__ == "__main__":
CP = car.CarParams(notCar=True, wheelbase=1, steerRatio=10)
Params().put("CarParams", CP.to_bytes(), block=True)
procs = ['camerad', 'ui', 'modeld', 'calibrationd', 'plannerd', 'dmonitoringmodeld', 'dmonitoringd']
for p in procs:
managed_processes[p].start()
pm = messaging.PubMaster(['controlsState', 'deviceState', 'pandaStates', 'carParams'])
msgs = {s: messaging.new_message(s) for s in ['controlsState', 'deviceState', 'carParams']}
msgs['deviceState'].deviceState.started = True
msgs['deviceState'].deviceState.deviceType = HARDWARE.get_device_type()
msgs['carParams'].carParams.openpilotLongitudinalControl = True
msgs['pandaStates'] = messaging.new_message('pandaStates', 1)
msgs['pandaStates'].pandaStates[0].ignitionLine = True
msgs['pandaStates'].pandaStates[0].pandaType = log.PandaState.PandaType.uno
try:
while True:
time.sleep(1 / 100) # continually send, rate doesn't matter
for s in msgs:
pm.send(s, msgs[s])
except KeyboardInterrupt:
for p in procs:
managed_processes[p].stop()
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env python3
import argparse
import datetime
import time
from collections import deque
from dataclasses import dataclass, field
import numpy as np
import openpilot.cereal.messaging as messaging
from openpilot.cereal.services import SERVICE_LIST
@dataclass
class ServiceTiming:
times: list[float] = field(default_factory=list)
window: deque[float] = field(default_factory=lambda: deque(maxlen=100))
valids: deque[bool] = field(default_factory=lambda: deque(maxlen=100))
lag_events: list[tuple[float, float]] = field(default_factory=list)
def add(self, mono_time: float, valid: bool, expected_interval: float | None, lag_threshold: float) -> None:
if self.times:
dt = mono_time - self.times[-1]
self.window.append(dt)
if expected_interval is not None and dt > lag_threshold * expected_interval:
self.lag_events.append((mono_time, dt))
self.times.append(mono_time)
self.valids.append(valid)
def intervals(self, latest_only: bool) -> np.ndarray:
if latest_only:
return np.array(self.window)
return np.diff(self.times)
def format_row(name: str, timing: ServiceTiming, latest_only: bool) -> str:
dts = timing.intervals(latest_only)
if len(dts) == 0:
return f"{name:25} waiting for messages"
mean = np.mean(dts)
hz = 1.0 / mean if mean > 0 else 0.0
valid = all(timing.valids) if timing.valids else False
return f"{name:25} {hz:8.2f}Hz {mean * 1e3:8.2f}ms {np.std(dts) * 1e3:8.2f}ms {np.max(dts) * 1e3:8.2f}ms {np.min(dts) * 1e3:8.2f}ms valid={valid}"
def print_lag_events(name: str, timing: ServiceTiming, printed_lags: dict[str, int]) -> None:
start = printed_lags.get(name, 0)
for mono_time, dt in timing.lag_events[start:]:
print(f"{mono_time:.3f} {name} lag {dt:.3f}s", flush=True)
printed_lags[name] = len(timing.lag_events)
def monitor_services(socket_names: list[str], print_interval: float, lag_threshold: float, lag_only: bool) -> None:
sockets = {name: messaging.sub_sock(name, conflate=False) for name in socket_names}
timings = {name: ServiceTiming() for name in socket_names}
printed_lags: dict[str, int] = {}
start_time = time.monotonic()
last_print = start_time
try:
while True:
for name, sock in sockets.items():
for msg in messaging.drain_sock(sock):
expected_interval = 1.0 / SERVICE_LIST[name].frequency if name in SERVICE_LIST else None
timings[name].add(msg.logMonoTime / 1e9, msg.valid, expected_interval, lag_threshold)
now = time.monotonic()
if now - last_print < print_interval:
time.sleep(0.01)
continue
if not lag_only:
print(flush=True)
print(f"{'service':25} {'freq':>10} {'mean':>10} {'std':>10} {'max':>10} {'min':>10} valid", flush=True)
for name in socket_names:
print(format_row(name, timings[name], latest_only=True), flush=True)
for name in socket_names:
print_lag_events(name, timings[name], printed_lags)
last_print = now
except KeyboardInterrupt:
print("\n", flush=True)
print("=" * 5, "timing summary", "=" * 5, flush=True)
print(f"{'service':25} {'freq':>10} {'mean':>10} {'std':>10} {'max':>10} {'min':>10} valid", flush=True)
for name in socket_names:
print(format_row(name, timings[name], latest_only=False), flush=True)
print("=" * 5, datetime.timedelta(seconds=time.monotonic() - start_time), "=" * 5, flush=True)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Check live service timing, frequency, validity, and lag")
parser.add_argument("socket", nargs="*", default=["carState"], help="service/socket name")
parser.add_argument("--lag-threshold", type=float, default=10.0, help="report intervals above this multiple of the expected service interval")
parser.add_argument("--lag-only", action="store_true", help="only print lag events")
parser.add_argument("--print-interval", type=float, default=1.0, help="seconds between table updates")
args = parser.parse_args()
monitor_services(args.socket, args.print_interval, args.lag_threshold, args.lag_only)