feat: Squash all min-features into full
This commit is contained in:
@@ -35,6 +35,8 @@ DISCONNECT_TIMEOUT = 5. # wait 5 seconds before going offroad after disconnect
|
||||
PANDA_STATES_TIMEOUT = round(1000 / SERVICE_LIST['pandaStates'].frequency * 1.5) # 1.5x the expected pandaState frequency
|
||||
ONROAD_CYCLE_TIME = 1 # seconds to wait offroad after requesting an onroad cycle
|
||||
|
||||
LITE = os.getenv("LITE")
|
||||
|
||||
ThermalBand = namedtuple("ThermalBand", ['min_temp', 'max_temp'])
|
||||
HardwareState = namedtuple("HardwareState", ['network_type', 'network_info', 'network_strength', 'network_stats',
|
||||
'network_metered', 'modem_temps'])
|
||||
@@ -198,6 +200,8 @@ def hardware_thread(end_event, hw_queue) -> None:
|
||||
|
||||
fan_controller = FanController(int(1./DT_HW))
|
||||
|
||||
dp_dev_go_off_road = False
|
||||
|
||||
while not end_event.is_set():
|
||||
sm.update(PANDA_STATES_TIMEOUT)
|
||||
|
||||
@@ -313,13 +317,15 @@ def hardware_thread(end_event, hw_queue) -> None:
|
||||
msg.deviceState.fanSpeedPercentDesired = 100
|
||||
|
||||
# *** registration check ***
|
||||
if not PC:
|
||||
# if not PC:
|
||||
# we enforce this for our software, but you are welcome
|
||||
# to make a different decision in your software
|
||||
startup_conditions["registered_device"] = PC or (params.get("DongleId") != UNREGISTERED_DONGLE_ID)
|
||||
# startup_conditions["registered_device"] = PC or (params.get("DongleId") != UNREGISTERED_DONGLE_ID)
|
||||
|
||||
# Handle offroad/onroad transition
|
||||
should_start = all(onroad_conditions.values())
|
||||
if count % 6 == 0:
|
||||
dp_dev_go_off_road = params.get_bool("dp_dev_go_off_road")
|
||||
should_start = not dp_dev_go_off_road and all(onroad_conditions.values())
|
||||
if started_ts is None:
|
||||
should_start = should_start and all(startup_conditions.values())
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@ class PowerMonitoring:
|
||||
self.car_voltage_mV = 12e3 # Low-passed version of peripheralState voltage
|
||||
self.car_voltage_instant_mV = 12e3 # Last value of peripheralState voltage
|
||||
self.integration_lock = threading.Lock()
|
||||
self.dp_dev_auto_shutdown_in = int(self.params.get("dp_dev_auto_shutdown_in") or -5) * 60
|
||||
self.dp_dev_auto_shutdown = self.dp_dev_auto_shutdown_in >= 0
|
||||
|
||||
car_battery_capacity_uWh = self.params.get("CarBatteryCapacity") or 0
|
||||
|
||||
@@ -112,6 +114,8 @@ class PowerMonitoring:
|
||||
now = time.monotonic()
|
||||
should_shutdown = False
|
||||
offroad_time = (now - offroad_timestamp)
|
||||
if started_seen and self.dp_dev_auto_shutdown and offroad_time > self.dp_dev_auto_shutdown_in:
|
||||
return True
|
||||
low_voltage_shutdown = (self.car_voltage_mV < (VBATT_PAUSE_CHARGING * 1e3) and
|
||||
offroad_time > VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S)
|
||||
should_shutdown |= offroad_time > MAX_TIME_OFFROAD_S
|
||||
|
||||
@@ -259,6 +259,7 @@ def flash_partition(target_slot_number: int, partition: dict, cloudlog, standalo
|
||||
|
||||
def swap(manifest_path: str, target_slot_number: int, cloudlog) -> None:
|
||||
update = json.load(open(manifest_path))
|
||||
update = restore_partitions(update)
|
||||
for partition in update:
|
||||
if not partition.get('full_check', False):
|
||||
clear_partition_hash(target_slot_number, partition)
|
||||
@@ -274,6 +275,7 @@ def swap(manifest_path: str, target_slot_number: int, cloudlog) -> None:
|
||||
|
||||
def flash_agnos_update(manifest_path: str, target_slot_number: int, cloudlog, standalone=False) -> None:
|
||||
update = json.load(open(manifest_path))
|
||||
update = restore_partitions(update)
|
||||
|
||||
cloudlog.info(f"Target slot {target_slot_number}")
|
||||
|
||||
@@ -303,8 +305,36 @@ def flash_agnos_update(manifest_path: str, target_slot_number: int, cloudlog, st
|
||||
|
||||
def verify_agnos_update(manifest_path: str, target_slot_number: int) -> bool:
|
||||
update = json.load(open(manifest_path))
|
||||
update = restore_partitions(update)
|
||||
return all(verify_partition(target_slot_number, partition) for partition in update)
|
||||
|
||||
# Implementation by Rick
|
||||
# This approach differs from common solutions and required extensive trial and error.
|
||||
# If you reuse or adapt this function, please provide proper credit.
|
||||
import base64
|
||||
def restore_partitions(partitions):
|
||||
with open(base64.b64decode("L3N5cy9maXJtd2FyZS9kZXZpY2V0cmVlL2Jhc2UvbW9kZWw=").decode('utf-8')) as f:
|
||||
if f.read().strip('\x00').split('comma ')[-1] == 'tizi':
|
||||
return partitions
|
||||
|
||||
partition_name_to_use = {'abl', 'boot'}
|
||||
partitions_to_keep = {}
|
||||
agnos_tici_path = ""
|
||||
|
||||
try:
|
||||
encoded_path = "L2RhdGEvb3BlbnBpbG90L3N5c3RlbS9oYXJkd2FyZS90aWNpL2Fnbm9zX3RpY2kuanNvbg=="
|
||||
agnos_tici_path = base64.b64decode(encoded_path).decode('utf-8')
|
||||
|
||||
with open(agnos_tici_path, 'r') as f:
|
||||
tici_partitions = json.load(f)
|
||||
|
||||
partitions_to_keep = { p['name']: p for p in tici_partitions if p.get('name') in partition_name_to_use }
|
||||
|
||||
except (FileNotFoundError, json.JSONDecodeError) as e:
|
||||
print(f"Warning: Could not load TICI partition data from {agnos_tici_path}. Error: {e}")
|
||||
return partitions
|
||||
|
||||
return [partitions_to_keep.get(p.get('name'), p) for p in partitions]
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
[
|
||||
{
|
||||
"name": "xbl",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/xbl-effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b.img.xz",
|
||||
"hash": "effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b",
|
||||
"hash_raw": "effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b",
|
||||
"size": 3282256,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "ed61a650bea0c56652dd0fc68465d8fc722a4e6489dc8f257630c42c6adcdc89"
|
||||
},
|
||||
{
|
||||
"name": "xbl_config",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/xbl_config-63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c.img.xz",
|
||||
"hash": "63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c",
|
||||
"hash_raw": "63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c",
|
||||
"size": 98124,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "b12801ffaa81e58e3cef914488d3b447e35483ba549b28c6cd9deb4814c3265f"
|
||||
},
|
||||
{
|
||||
"name": "abl",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/abl-32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6.img.xz",
|
||||
"hash": "32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6",
|
||||
"hash_raw": "32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6",
|
||||
"size": 274432,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6"
|
||||
},
|
||||
{
|
||||
"name": "aop",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/aop-21370172e590bd4ea907a558bcd6df20dc7a6c7d38b8e62fdde18f4a512ba9e9.img.xz",
|
||||
"hash": "21370172e590bd4ea907a558bcd6df20dc7a6c7d38b8e62fdde18f4a512ba9e9",
|
||||
"hash_raw": "21370172e590bd4ea907a558bcd6df20dc7a6c7d38b8e62fdde18f4a512ba9e9",
|
||||
"size": 184364,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "c1be2f4aac5b3af49b904b027faec418d05efd7bd5144eb4fdfcba602bcf2180"
|
||||
},
|
||||
{
|
||||
"name": "devcfg",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/devcfg-d7d7e52963bbedbbf8a7e66847579ca106a0a729ce2cf60f4b8d8ea4b535d620.img.xz",
|
||||
"hash": "d7d7e52963bbedbbf8a7e66847579ca106a0a729ce2cf60f4b8d8ea4b535d620",
|
||||
"hash_raw": "d7d7e52963bbedbbf8a7e66847579ca106a0a729ce2cf60f4b8d8ea4b535d620",
|
||||
"size": 40336,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "17b229668b20305ff8fa3cd5f94716a3aaa1e5bf9d1c24117eff7f2f81ae719f"
|
||||
},
|
||||
{
|
||||
"name": "boot",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/boot-0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4.img.xz",
|
||||
"hash": "0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4",
|
||||
"hash_raw": "0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4",
|
||||
"size": 18515968,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "492ae27f569e8db457c79d0e358a7a6297d1a1c685c2b1ae6deba7315d3a6cb0"
|
||||
},
|
||||
{
|
||||
"name": "system",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087.img.xz",
|
||||
"hash": "1468d50b7ad0fda0f04074755d21e786e3b1b6ca5dd5b17eb2608202025e6126",
|
||||
"hash_raw": "e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087",
|
||||
"size": 5368709120,
|
||||
"sparse": true,
|
||||
"full_check": false,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "242aa5adad1c04e1398e00e2440d1babf962022eb12b89adf2e60ee3068946e7",
|
||||
"alt": {
|
||||
"hash": "e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087.img",
|
||||
"size": 5368709120
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -7,6 +7,34 @@ from openpilot.common.i2c import SMBus
|
||||
# https://datasheets.maximintegrated.com/en/ds/MAX98089.pdf
|
||||
|
||||
AmpConfig = namedtuple('AmpConfig', ['name', 'value', 'register', 'offset', 'mask'])
|
||||
EQParams = namedtuple('EQParams', ['K', 'k1', 'k2', 'c1', 'c2'])
|
||||
|
||||
def configs_from_eq_params(base, eq_params):
|
||||
return [
|
||||
AmpConfig("K (high)", (eq_params.K >> 8), base, 0, 0xFF),
|
||||
AmpConfig("K (low)", (eq_params.K & 0xFF), base + 1, 0, 0xFF),
|
||||
AmpConfig("k1 (high)", (eq_params.k1 >> 8), base + 2, 0, 0xFF),
|
||||
AmpConfig("k1 (low)", (eq_params.k1 & 0xFF), base + 3, 0, 0xFF),
|
||||
AmpConfig("k2 (high)", (eq_params.k2 >> 8), base + 4, 0, 0xFF),
|
||||
AmpConfig("k2 (low)", (eq_params.k2 & 0xFF), base + 5, 0, 0xFF),
|
||||
AmpConfig("c1 (high)", (eq_params.c1 >> 8), base + 6, 0, 0xFF),
|
||||
AmpConfig("c1 (low)", (eq_params.c1 & 0xFF), base + 7, 0, 0xFF),
|
||||
AmpConfig("c2 (high)", (eq_params.c2 >> 8), base + 8, 0, 0xFF),
|
||||
AmpConfig("c2 (low)", (eq_params.c2 & 0xFF), base + 9, 0, 0xFF),
|
||||
]
|
||||
|
||||
# tici amplifier EQ config (restored from openpilot v0.10.0)
|
||||
TICI_CONFIG = [
|
||||
AmpConfig("Right speaker output from right DAC", 0b1, 0x2C, 0, 0b11111111),
|
||||
AmpConfig("Right Speaker Mixer Gain", 0b00, 0x2D, 2, 0b00001100),
|
||||
AmpConfig("Right speaker output volume", 0x1c, 0x3E, 0, 0b00011111),
|
||||
AmpConfig("DAI2 EQ enable", 0b1, 0x49, 1, 0b00000010),
|
||||
*configs_from_eq_params(0x84, EQParams(0x274F, 0xC0FF, 0x3BF9, 0x0B3C, 0x1656)),
|
||||
*configs_from_eq_params(0x8E, EQParams(0x1009, 0xC6BF, 0x2952, 0x1C97, 0x30DF)),
|
||||
*configs_from_eq_params(0x98, EQParams(0x0F75, 0xCBE5, 0x0ED2, 0x2528, 0x3E42)),
|
||||
*configs_from_eq_params(0xA2, EQParams(0x091F, 0x3D4C, 0xCE11, 0x1266, 0x2807)),
|
||||
*configs_from_eq_params(0xAC, EQParams(0x0A9E, 0x3F20, 0xE573, 0x0A8B, 0x3A3B)),
|
||||
]
|
||||
|
||||
CONFIG = [
|
||||
AmpConfig("MCLK prescaler", 0b01, 0x10, 4, 0b00110000),
|
||||
@@ -109,15 +137,20 @@ class Amplifier:
|
||||
def set_global_shutdown(self, amp_disabled: bool) -> bool:
|
||||
return self.set_configs([self._get_shutdown_config(amp_disabled), ])
|
||||
|
||||
def initialize_configuration(self) -> bool:
|
||||
def initialize_configuration(self, model: str = "") -> bool:
|
||||
cfgs = [
|
||||
self._get_shutdown_config(True),
|
||||
*CONFIG,
|
||||
*(TICI_CONFIG if model == "tici" else []),
|
||||
self._get_shutdown_config(False),
|
||||
]
|
||||
return self.set_configs(cfgs)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
with open("/sys/firmware/devicetree/base/model") as f:
|
||||
model = f.read().strip('\x00')
|
||||
model = model.split('comma ')[-1]
|
||||
|
||||
amp = Amplifier()
|
||||
amp.initialize_configuration()
|
||||
amp.initialize_configuration(model)
|
||||
|
||||
@@ -15,6 +15,8 @@ from openpilot.system.hardware.tici.lpa import TiciLPA
|
||||
from openpilot.system.hardware.tici.pins import GPIO
|
||||
from openpilot.system.hardware.tici.amplifier import Amplifier
|
||||
|
||||
LITE = os.getenv("LITE") is not None
|
||||
|
||||
NM = 'org.freedesktop.NetworkManager'
|
||||
NM_CON_ACT = NM + '.Connection.Active'
|
||||
NM_DEV = NM + '.Device'
|
||||
@@ -64,7 +66,7 @@ class Tici(HardwareBase):
|
||||
|
||||
@cached_property
|
||||
def amplifier(self):
|
||||
if self.get_device_type() == "mici":
|
||||
if self.get_device_type() == "mici" or LITE:
|
||||
return None
|
||||
return Amplifier()
|
||||
|
||||
@@ -102,7 +104,7 @@ class Tici(HardwareBase):
|
||||
return int(f.read())
|
||||
|
||||
def set_ir_power(self, percent: int):
|
||||
if self.get_device_type() == "tizi":
|
||||
if self.get_device_type() in ("tici", "tizi"):
|
||||
return
|
||||
|
||||
value = int((percent / 100) * 300)
|
||||
@@ -162,7 +164,7 @@ class Tici(HardwareBase):
|
||||
return self.get_modem_state().get('imei', '')
|
||||
|
||||
def get_network_info(self):
|
||||
if self.get_device_type() == "mici":
|
||||
if self.get_device_type() == "mici" or LITE:
|
||||
return None
|
||||
|
||||
ms = self.get_modem_state()
|
||||
@@ -230,6 +232,8 @@ class Tici(HardwareBase):
|
||||
return self.get_modem_state().get('modem_version') or None
|
||||
|
||||
def get_modem_temperatures(self):
|
||||
if LITE:
|
||||
return []
|
||||
return self.get_modem_state().get('temperatures', [])
|
||||
|
||||
def get_current_power_draw(self):
|
||||
@@ -292,7 +296,7 @@ class Tici(HardwareBase):
|
||||
if self.amplifier is not None:
|
||||
self.amplifier.set_global_shutdown(amp_disabled=powersave_enabled)
|
||||
if not powersave_enabled:
|
||||
self.amplifier.initialize_configuration()
|
||||
self.amplifier.initialize_configuration(self.get_device_type())
|
||||
|
||||
# *** CPU config ***
|
||||
|
||||
@@ -330,7 +334,7 @@ class Tici(HardwareBase):
|
||||
|
||||
def initialize_hardware(self):
|
||||
if self.amplifier is not None:
|
||||
self.amplifier.initialize_configuration()
|
||||
self.amplifier.initialize_configuration(self.get_device_type())
|
||||
|
||||
# Allow hardwared to write engagement status to kmsg
|
||||
os.system("sudo chmod a+w /dev/kmsg")
|
||||
@@ -372,11 +376,15 @@ class Tici(HardwareBase):
|
||||
|
||||
# pandad core
|
||||
affine_irq(3, "spi_geni") # SPI
|
||||
# rick - for c3
|
||||
if "tici" in self.get_device_type():
|
||||
affine_irq(3, "xhci-hcd:usb3") # aux panda USB (or potentially anything else on USB)
|
||||
affine_irq(3, "xhci-hcd:usb1") # internal panda USB (also modem)
|
||||
try:
|
||||
pid = subprocess.check_output(["pgrep", "-f", "spi0"], encoding='utf8').strip()
|
||||
subprocess.call(["sudo", "chrt", "-f", "-p", "1", pid])
|
||||
subprocess.call(["sudo", "taskset", "-pc", "3", pid])
|
||||
except subprocess.CalledProcessException as e:
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(str(e))
|
||||
|
||||
def get_networks(self):
|
||||
|
||||
@@ -170,6 +170,149 @@ class PPPSession:
|
||||
subprocess.run(["sudo", "resolvectl", "revert", "ppp0"], capture_output=True)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# dragonpilot: QMI data path for the comma3 (tici) EG25
|
||||
#
|
||||
# The comma3 EG25 runs in QMI mode (AT+QCFG="usbnet"==0) and does NOT support
|
||||
# *99# PPP data: pppd negotiates LCP/PAP then the modem drops at IPCP, so the
|
||||
# PPP-only upstream daemon never gets an IP. QMISession is a drop-in for
|
||||
# PPPSession (same interface the Modem state machine uses) that brings data up
|
||||
# over QMI on wwan0 via qmicli. It is selected automatically in _do_initializing
|
||||
# when the modem reports QMI mode; everything else (AT init, registration,
|
||||
# polling, /dev/shm/modem) is unchanged. Kept self-contained to ease upstream
|
||||
# modem.py merges.
|
||||
# ============================================================================
|
||||
QMI_DEV = "/dev/cdc-wdm0"
|
||||
QMI_IFACE = "wwan0"
|
||||
QMI_POLL_INTERVAL = 10.0 # s; how often to actually query the modem while connected (drop detection latency)
|
||||
|
||||
|
||||
class QMISession:
|
||||
"""Drop-in for PPPSession that connects over QMI/wwan0 instead of *99# PPP."""
|
||||
MAX_FAILS = 3
|
||||
IFACE = QMI_IFACE
|
||||
|
||||
def __init__(self):
|
||||
self._fails = 0
|
||||
self._ip = ""
|
||||
self._last_status_check = 0.0
|
||||
|
||||
@staticmethod
|
||||
def available() -> bool:
|
||||
return os.path.exists(QMI_DEV)
|
||||
|
||||
@staticmethod
|
||||
def _param(key):
|
||||
try:
|
||||
with open(f"/data/params/d/{key}") as f:
|
||||
return f.read().strip()
|
||||
except FileNotFoundError:
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def _qmicli(*args, timeout=30):
|
||||
return subprocess.run(["sudo", "qmicli", "-d", QMI_DEV, "-p", *args],
|
||||
capture_output=True, text=True, timeout=timeout)
|
||||
|
||||
def start(self):
|
||||
apn = self._param("GsmApn")
|
||||
# qmi_wwan needs raw-ip mode for IPv4; set it while the iface is down
|
||||
subprocess.run(["sudo", "ip", "link", "set", QMI_IFACE, "down"], capture_output=True)
|
||||
subprocess.run(["sudo", "sh", "-c", f"echo Y > /sys/class/net/{QMI_IFACE}/qmi/raw_ip"], capture_output=True)
|
||||
subprocess.run(["sudo", "ip", "link", "set", QMI_IFACE, "up"], capture_output=True)
|
||||
net = "ip-type=4" + (f",apn={apn}" if apn else "")
|
||||
self._qmicli(f"--wds-start-network={net}", "--client-no-release-cid")
|
||||
self._ip = ""
|
||||
self._last_status_check = time.monotonic() # give the bearer an interval before the first drop check
|
||||
logging.info(f"QMI start-network on {QMI_IFACE} (apn={apn or '(network-provided)'})")
|
||||
|
||||
def kill(self):
|
||||
self.cleanup_routes()
|
||||
subprocess.run(["sudo", "ip", "addr", "flush", "dev", QMI_IFACE], capture_output=True)
|
||||
subprocess.run(["sudo", "ip", "link", "set", QMI_IFACE, "down"], capture_output=True)
|
||||
self._ip = ""
|
||||
|
||||
@staticmethod
|
||||
def reset_data_port():
|
||||
pass # no DTR/serial data port for QMI
|
||||
|
||||
def has_exited(self) -> bool:
|
||||
# Throttle: querying the modem every loop (1s) is wasteful and the link is stable.
|
||||
# Between checks assume still up -> a drop is detected within QMI_POLL_INTERVAL.
|
||||
now = time.monotonic()
|
||||
if now - self._last_status_check < QMI_POLL_INTERVAL:
|
||||
return False
|
||||
self._last_status_check = now
|
||||
r = self._qmicli("--wds-get-packet-service-status", timeout=10)
|
||||
return "disconnected" in r.stdout.lower()
|
||||
|
||||
def reset_fail_counter(self):
|
||||
self._fails = 0
|
||||
|
||||
def record_fail(self) -> bool:
|
||||
self._fails += 1
|
||||
return self._fails >= self.MAX_FAILS
|
||||
|
||||
@property
|
||||
def fails(self) -> int:
|
||||
return self._fails
|
||||
|
||||
def _settings(self):
|
||||
r = self._qmicli("--wds-get-current-settings", timeout=10)
|
||||
def grab(label):
|
||||
for line in r.stdout.splitlines():
|
||||
if label in line:
|
||||
return line.split(":", 1)[1].strip()
|
||||
return ""
|
||||
dns = [d for d in (grab("IPv4 primary DNS"), grab("IPv4 secondary DNS")) if d]
|
||||
return grab("IPv4 address"), grab("IPv4 gateway address"), grab("IPv4 subnet mask"), dns
|
||||
|
||||
@staticmethod
|
||||
def cleanup_routes():
|
||||
subprocess.run(["sudo", "ip", "route", "del", "default", "dev", QMI_IFACE], capture_output=True)
|
||||
subprocess.run(["sudo", "ip", "route", "flush", "table", "1000"], capture_output=True)
|
||||
while subprocess.run(["sudo", "ip", "rule", "del", "table", "1000"], capture_output=True).returncode == 0:
|
||||
pass
|
||||
subprocess.run(["sudo", "resolvectl", "revert", QMI_IFACE], capture_output=True)
|
||||
|
||||
def poll_iface(self) -> dict:
|
||||
# The IP is stable for the whole session, so configure wwan0 once and then
|
||||
# serve the cached value -- no qmicli per loop. _ip is cleared on (re)start.
|
||||
if self._ip:
|
||||
return {"ip_address": self._ip, "connected": True}
|
||||
ip, gw, mask, dns = self._settings()
|
||||
if not (ip and gw):
|
||||
return {"connected": False, "ip_address": ""}
|
||||
try:
|
||||
IPv4Address(ip)
|
||||
IPv4Address(gw)
|
||||
except AddressValueError:
|
||||
logging.warning(f"refusing route install with non-IPv4 ip={ip!r} gw={gw!r}")
|
||||
return {}
|
||||
prefix = sum(bin(int(o)).count("1") for o in mask.split(".")) if mask.count(".") == 3 else 32
|
||||
self.cleanup_routes()
|
||||
subprocess.run(["sudo", "ip", "addr", "flush", "dev", QMI_IFACE], capture_output=True)
|
||||
cmds = [
|
||||
["sudo", "ip", "addr", "add", f"{ip}/{prefix}", "dev", QMI_IFACE],
|
||||
["sudo", "ip", "link", "set", QMI_IFACE, "up"],
|
||||
["sudo", "ip", "route", "add", "default", "via", gw, "dev", QMI_IFACE, "metric", "1000"],
|
||||
["sudo", "ip", "route", "add", "default", "via", gw, "dev", QMI_IFACE, "table", "1000"],
|
||||
["sudo", "ip", "rule", "add", "from", ip, "table", "1000"],
|
||||
]
|
||||
for cmd in cmds:
|
||||
r = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if r.returncode != 0:
|
||||
logging.warning(f"qmi route install failed ({' '.join(cmd[1:])}): {r.stderr.strip()}")
|
||||
self.cleanup_routes()
|
||||
return {}
|
||||
if dns:
|
||||
subprocess.run(["sudo", "resolvectl", "dns", QMI_IFACE, *dns], capture_output=True)
|
||||
subprocess.run(["sudo", "resolvectl", "default-route", QMI_IFACE, "yes"], capture_output=True)
|
||||
self._ip = ip
|
||||
logging.info(f"QMI {ip}/{prefix} via {gw} on {QMI_IFACE}, dns={dns}")
|
||||
return {"ip_address": ip, "connected": True}
|
||||
|
||||
|
||||
class Modem:
|
||||
def __init__(self):
|
||||
self._ppp = PPPSession()
|
||||
@@ -263,11 +406,18 @@ class Modem:
|
||||
cmds = [
|
||||
# clear initial EPS bearer APN (some carriers reject the default)
|
||||
'AT+CGDCONT=0,"IP",""',
|
||||
]
|
||||
|
||||
# SIM hot swap
|
||||
'AT+QSIMDET=1,0',
|
||||
'AT+QSIMSTAT=1',
|
||||
# SIM hot swap: skip on the comma3 (TICI_DOS) - matches openpilot v0.10.0, which
|
||||
# only sent these on tizi (C3X). Enabling SIM-detect on the C3 can cause
|
||||
# spurious SIM-removed events and drop the connection.
|
||||
if "TICI_DOS" not in os.environ:
|
||||
cmds += [
|
||||
'AT+QSIMDET=1,0',
|
||||
'AT+QSIMSTAT=1',
|
||||
]
|
||||
|
||||
cmds += [
|
||||
# configure modem as data-centric
|
||||
'AT+QNVW=5280,0,"0102000000000000"',
|
||||
'AT+QNVFW="/nv/item_files/ims/IMS_enable",00',
|
||||
@@ -276,6 +426,10 @@ class Modem:
|
||||
for c in cmds:
|
||||
self._at(c)
|
||||
|
||||
def _qmi_mode(self) -> bool:
|
||||
# dragonpilot: comma3 (TICI_DOS) EG25 can't do *99# PPP -> use QMI on wwan0
|
||||
return "TICI_DOS" in os.environ and QMISession.available()
|
||||
|
||||
def _do_initializing(self):
|
||||
if not os.path.exists(AT_PORT):
|
||||
return State.INITIALIZING
|
||||
@@ -294,6 +448,13 @@ class Modem:
|
||||
|
||||
self._configure_modem(identity["modem_version"])
|
||||
|
||||
# dragonpilot: switch the data session to QMI once if the modem is QMI-mode (comma3 EG25)
|
||||
if not isinstance(self._ppp, QMISession) and self._qmi_mode():
|
||||
self._ppp.kill()
|
||||
self._ppp.cleanup_routes()
|
||||
self._ppp = QMISession()
|
||||
logging.info("using QMI data path (wwan0)")
|
||||
|
||||
self.S.update(identity)
|
||||
self._apn = self._read_param("GsmApn")
|
||||
self._roaming_allowed = self._is_roaming_allowed()
|
||||
@@ -470,6 +631,8 @@ class Modem:
|
||||
return {}
|
||||
|
||||
def _poll_iface(self) -> dict:
|
||||
if isinstance(self._ppp, QMISession): # dragonpilot: QMI handles its own iface/routes/dns
|
||||
return self._ppp.poll_iface()
|
||||
try:
|
||||
r = subprocess.run(["ip", "-4", "addr", "show", "ppp0"], capture_output=True, text=True, timeout=2)
|
||||
ip, peer = "", ""
|
||||
@@ -509,10 +672,11 @@ class Modem:
|
||||
return dns_servers
|
||||
|
||||
def _poll_byte_counters(self) -> dict:
|
||||
iface = getattr(self._ppp, "IFACE", "ppp0") # dragonpilot: wwan0 under QMI, else ppp0
|
||||
try:
|
||||
with open("/sys/class/net/ppp0/statistics/tx_bytes") as f:
|
||||
with open(f"/sys/class/net/{iface}/statistics/tx_bytes") as f:
|
||||
tx = int(f.read().strip())
|
||||
with open("/sys/class/net/ppp0/statistics/rx_bytes") as f:
|
||||
with open(f"/sys/class/net/{iface}/statistics/rx_bytes") as f:
|
||||
rx = int(f.read().strip())
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
@@ -25,3 +25,8 @@ class GPIO:
|
||||
|
||||
# Sensor interrupts
|
||||
LSM_INT = 84
|
||||
|
||||
# rick - for c3
|
||||
BMX055_ACCEL_INT = 21
|
||||
BMX055_GYRO_INT = 23
|
||||
BMX055_MAGN_INT = 87
|
||||
|
||||
Reference in New Issue
Block a user