IQ.Pilot Release Commit @ 0babf78
This commit is contained in:
34
system/flockd.service
Normal file
34
system/flockd.service
Normal file
@@ -0,0 +1,34 @@
|
||||
# Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
|
||||
[Unit]
|
||||
Description=Konn3kt Flock/ALPR RF Detector
|
||||
Documentation=https://gitlvb.teallvbs.xyz/teal/iqpilot
|
||||
After=bluetooth.service dbus.service NetworkManager.service
|
||||
Wants=bluetooth.service dbus.service
|
||||
StartLimitIntervalSec=300
|
||||
StartLimitBurst=10
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=comma
|
||||
AmbientCapabilities=CAP_SYS_NICE
|
||||
Environment="IQPILOT_SOURCE_ROOT=/data/openpilot/openpilot"
|
||||
Environment="PYTHONPATH=/usr/libexec/iqpilot/python:/data/openpilot"
|
||||
Environment="PYTHONSAFEPATH=1"
|
||||
Environment="PATH=/usr/local/venv/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
WorkingDirectory=/data/openpilot
|
||||
ExecStartPre=/bin/bash -c 'for i in $(seq 1 120); do if [ -x /usr/libexec/iqpilot/iqpilot_bundle_runner ]; then exit 0; fi; echo "Waiting for iqpilot_bundle_runner..."; sleep 5; done; exit 1'
|
||||
ExecStartPre=/bin/bash -c 'if [ -f /data/openpilot/artifacts/runtime/ensure_private_installed.sh ]; then bash /data/openpilot/artifacts/runtime/ensure_private_installed.sh || true; fi'
|
||||
ExecStart=/bin/bash -lc 'exec /usr/libexec/iqpilot/iqpilot_bundle_runner --bundle iqpilot_hephaestusd_private --mode python-module --entry iqpilot_private.konn3kt.flockd.flockd --daemon-name flockd'
|
||||
TimeoutStartSec=600
|
||||
Restart=always
|
||||
RestartSec=15
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
PrivateTmp=yes
|
||||
NoNewPrivileges=false
|
||||
ProtectSystem=full
|
||||
ProtectHome=no
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -200,6 +200,9 @@ class HardwareBase(ABC):
|
||||
def reboot_modem(self):
|
||||
pass
|
||||
|
||||
def recover_sim_detection(self) -> bool:
|
||||
return False
|
||||
|
||||
def get_networks(self):
|
||||
return None
|
||||
|
||||
|
||||
@@ -204,6 +204,7 @@ def hw_state_thread(end_event, hw_queue):
|
||||
modem_configured = False
|
||||
modem_missing_count = 0
|
||||
modem_restart_count = 0
|
||||
sim_detection_recovered = False
|
||||
|
||||
while not end_event.is_set():
|
||||
# these are expensive calls. update every 10s
|
||||
@@ -254,6 +255,10 @@ def hw_state_thread(end_event, hw_queue):
|
||||
HARDWARE.configure_modem()
|
||||
modem_configured = True
|
||||
|
||||
if modem_configured and not sim_detection_recovered and HARDWARE.recover_sim_detection():
|
||||
cloudlog.event("sim missing with hot-swap detect armed, rebooting modem with detect disabled", error=True)
|
||||
sim_detection_recovered = True
|
||||
|
||||
prev_hw_state = hw_state
|
||||
except Exception:
|
||||
cloudlog.exception("Error getting hardware state")
|
||||
@@ -462,7 +467,7 @@ def hardware_thread(end_event, hw_queue) -> None:
|
||||
startup_conditions["device_booted"] = startup_conditions.get("device_booted", False) or HARDWARE.booted()
|
||||
|
||||
# user-forced status (Always Offroad can be temporarily overridden)
|
||||
offroad_mode = params.get_bool("OffroadMode")
|
||||
offroad_mode = params.get_bool("IQAlwaysOffroad")
|
||||
force_onroad_until = params.get("ForceOnroadUntil", return_default=True)
|
||||
now = int(time.time())
|
||||
force_onroad_active = offroad_mode and force_onroad_until > now
|
||||
@@ -549,15 +554,17 @@ def hardware_thread(end_event, hw_queue) -> None:
|
||||
som_power_draw = HARDWARE.get_som_power_draw()
|
||||
msg.deviceState.somPowerDrawW = som_power_draw
|
||||
|
||||
# FastSleep deep standby: shed heavy processes at low battery instead of shutting down,
|
||||
# recover on ignition or once the alternator is charging
|
||||
# FastSleep deep standby: shed heavy processes once parked with the screen idled off
|
||||
# (or at low battery) instead of shutting down, recover on ignition or once the
|
||||
# alternator is charging
|
||||
fast_sleep = params.get_bool("FastSleep")
|
||||
if fast_sleep and not tesla_no_sleep:
|
||||
if low_power:
|
||||
if onroad_conditions["ignition"] or power_monitor.car_voltage_mV >= (VBATT_LOW_POWER_EXIT * 1e3):
|
||||
low_power = False
|
||||
else:
|
||||
low_power = power_monitor.should_enter_low_power(onroad_conditions["ignition"], in_car, off_ts)
|
||||
screen_off = msg.deviceState.screenBrightnessPercent < 1e-3
|
||||
low_power = power_monitor.should_enter_low_power(onroad_conditions["ignition"], in_car, off_ts, screen_off)
|
||||
else:
|
||||
low_power = False
|
||||
|
||||
|
||||
@@ -13,11 +13,13 @@ CAR_CHARGING_RATE_W = 45
|
||||
|
||||
VBATT_PAUSE_CHARGING = 11.8 # Lower limit on the LPF car battery voltage
|
||||
|
||||
# FastSleep (deep standby): enter low power at the normal shutdown voltage, shut down
|
||||
# at a lower floor, exit once the alternator is charging
|
||||
# FastSleep (deep standby): enter low power once parked with the screen idled off, or
|
||||
# immediately at the normal shutdown voltage; shut down at a lower floor, exit once the
|
||||
# alternator is charging
|
||||
VBATT_LOW_POWER_ENTRY = 11.8
|
||||
VBATT_LOW_POWER_EXIT = 12.8
|
||||
VBATT_HARD_SHUTDOWN = 11.5
|
||||
LOW_POWER_ENTRY_TIME_S = 300
|
||||
MAX_TIME_OFFROAD_S = 30*3600
|
||||
MIN_ON_TIME_S = 3600
|
||||
DELAY_SHUTDOWN_TIME_S = 300 # Wait at least DELAY_SHUTDOWN_TIME_S seconds after offroad_time to shutdown.
|
||||
@@ -124,14 +126,18 @@ class PowerMonitoring:
|
||||
return 0 < iq_max_time_val_s <= offroad_time
|
||||
|
||||
# FastSleep: see if we should enter low power mode instead of shutting down
|
||||
def should_enter_low_power(self, ignition: bool, in_car: bool, offroad_timestamp: float | None) -> bool:
|
||||
def should_enter_low_power(self, ignition: bool, in_car: bool, offroad_timestamp: float | None, screen_off: bool) -> bool:
|
||||
if offroad_timestamp is None or ignition or not in_car:
|
||||
return False
|
||||
if not self.params.get_bool("FastSleep"):
|
||||
return False
|
||||
offroad_time = time.monotonic() - offroad_timestamp
|
||||
return (self.car_voltage_mV < (VBATT_LOW_POWER_ENTRY * 1e3) and
|
||||
offroad_time > VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S)
|
||||
# a healthy battery rests above VBATT_LOW_POWER_ENTRY, so parked entry must be
|
||||
# time-based; the voltage trigger stays as the sagging-battery fast path
|
||||
low_voltage = (self.car_voltage_mV < (VBATT_LOW_POWER_ENTRY * 1e3) and
|
||||
offroad_time > VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S)
|
||||
parked_idle = screen_off and offroad_time > LOW_POWER_ENTRY_TIME_S
|
||||
return low_voltage or parked_idle
|
||||
|
||||
# See if we need to shutdown
|
||||
def should_shutdown(self, ignition: bool, in_car: bool, offroad_timestamp: float | None, started_seen: bool):
|
||||
@@ -141,12 +147,15 @@ class PowerMonitoring:
|
||||
now = time.monotonic()
|
||||
should_shutdown = False
|
||||
offroad_time = (now - offroad_timestamp)
|
||||
vbatt_min = VBATT_HARD_SHUTDOWN if self.params.get_bool("FastSleep") else VBATT_PAUSE_CHARGING
|
||||
fast_sleep = self.params.get_bool("FastSleep")
|
||||
vbatt_min = VBATT_HARD_SHUTDOWN if fast_sleep else VBATT_PAUSE_CHARGING
|
||||
low_voltage_shutdown = (self.car_voltage_mV < (vbatt_min * 1e3) and
|
||||
offroad_time > VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S)
|
||||
should_shutdown |= self.max_time_offroad_exceeded(offroad_time)
|
||||
should_shutdown |= low_voltage_shutdown
|
||||
should_shutdown |= (self.car_battery_capacity_uWh <= 0)
|
||||
# the 30 Wh bookkeeping model empties within hours at offroad draw regardless of the
|
||||
# real battery state; under FastSleep the measured voltage floors govern instead
|
||||
should_shutdown |= (self.car_battery_capacity_uWh <= 0) and not fast_sleep
|
||||
should_shutdown &= not ignition
|
||||
should_shutdown &= (not self.params.get_bool("DisablePowerDown"))
|
||||
should_shutdown &= in_car
|
||||
|
||||
@@ -2,7 +2,8 @@ import pytest
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.hardware.power_monitoring import PowerMonitoring, CAR_BATTERY_CAPACITY_uWh, \
|
||||
CAR_CHARGING_RATE_W, VBATT_PAUSE_CHARGING, DELAY_SHUTDOWN_TIME_S, MAX_TIME_OFFROAD_S
|
||||
CAR_CHARGING_RATE_W, VBATT_PAUSE_CHARGING, DELAY_SHUTDOWN_TIME_S, MAX_TIME_OFFROAD_S, \
|
||||
VBATT_HARD_SHUTDOWN, LOW_POWER_ENTRY_TIME_S
|
||||
|
||||
# Create fake time
|
||||
ssb = 0.
|
||||
@@ -232,3 +233,59 @@ class TestPowerMonitoring:
|
||||
result = pm.max_time_offroad_exceeded(offroad_time_s)
|
||||
|
||||
assert result == expected_result
|
||||
|
||||
# FastSleep must not shut down on the empty bookkeeping model while voltage is healthy
|
||||
def test_fast_sleep_ignores_battery_capacity_model(self, mocker):
|
||||
self.params.put_bool("FastSleep", True)
|
||||
self.params.put("MaxTimeOffroad", 0)
|
||||
try:
|
||||
pm_patch(mocker, "HARDWARE.get_current_power_draw", 0)
|
||||
pm = PowerMonitoring()
|
||||
pm.car_battery_capacity_uWh = 0
|
||||
start_time = ssb
|
||||
for _ in range(DELAY_SHUTDOWN_TIME_S + 100):
|
||||
pm.calculate(GOOD_VOLTAGE, False)
|
||||
assert not pm.should_shutdown(False, True, start_time, True)
|
||||
finally:
|
||||
self.params.put_bool("FastSleep", False)
|
||||
|
||||
# FastSleep still shuts down below the hard voltage floor
|
||||
def test_fast_sleep_hard_voltage_floor(self, mocker):
|
||||
self.params.put_bool("FastSleep", True)
|
||||
try:
|
||||
pm_patch(mocker, "HARDWARE.get_current_power_draw", 0)
|
||||
pm = PowerMonitoring()
|
||||
pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh
|
||||
start_time = ssb
|
||||
for _ in range(DELAY_SHUTDOWN_TIME_S + 100):
|
||||
pm.calculate((VBATT_HARD_SHUTDOWN - 0.5) * 1e3, False)
|
||||
assert pm.should_shutdown(False, True, start_time, True)
|
||||
finally:
|
||||
self.params.put_bool("FastSleep", False)
|
||||
|
||||
def test_fast_sleep_low_power_entry(self, mocker):
|
||||
self.params.put_bool("FastSleep", True)
|
||||
try:
|
||||
pm_patch(mocker, "HARDWARE.get_current_power_draw", 0)
|
||||
|
||||
# parked with the screen idled off: time-based entry at healthy voltage
|
||||
pm = PowerMonitoring()
|
||||
start_time = ssb
|
||||
for _ in range(LOW_POWER_ENTRY_TIME_S + 10):
|
||||
pm.calculate(GOOD_VOLTAGE, False)
|
||||
assert pm.should_enter_low_power(False, True, start_time, screen_off=True)
|
||||
assert not pm.should_enter_low_power(False, True, start_time, screen_off=False)
|
||||
assert not pm.should_enter_low_power(True, True, start_time, screen_off=True)
|
||||
assert not pm.should_enter_low_power(False, False, start_time, screen_off=True)
|
||||
|
||||
# sagging battery: voltage entry regardless of screen state
|
||||
pm = PowerMonitoring()
|
||||
start_time = ssb
|
||||
for _ in range(100):
|
||||
pm.calculate((VBATT_HARD_SHUTDOWN + 0.1) * 1e3, False)
|
||||
assert pm.should_enter_low_power(False, True, start_time, screen_off=False)
|
||||
|
||||
self.params.put_bool("FastSleep", False)
|
||||
assert not pm.should_enter_low_power(False, True, start_time, screen_off=True)
|
||||
finally:
|
||||
self.params.put_bool("FastSleep", False)
|
||||
|
||||
@@ -78,17 +78,17 @@
|
||||
},
|
||||
{
|
||||
"name": "system",
|
||||
"url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/system-036bb0bcf945115c6488efadcdb56a42192201290a5fe4572ac7b46549146c79.img.xz",
|
||||
"hash": "a0bf6d22e1134fc6c47158d63901c06324ea3b3808ed2fec35801a888cf3d526",
|
||||
"hash_raw": "036bb0bcf945115c6488efadcdb56a42192201290a5fe4572ac7b46549146c79",
|
||||
"url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/system-03397de3da2e9a6d2808b95b72c945b3af71ab79f7f0298624aed62b8749fc5a.img.xz",
|
||||
"hash": "0c9e7dee6c7365600c77b33c4996456459d04aad005e3e41bf6ee2e8af74ceb9",
|
||||
"hash_raw": "03397de3da2e9a6d2808b95b72c945b3af71ab79f7f0298624aed62b8749fc5a",
|
||||
"size": 6291456000,
|
||||
"sparse": true,
|
||||
"full_check": false,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "9a08b97618dceceed48205ec674330bb97c7f76487a6cca2f68288aaa42aaf8e",
|
||||
"ondevice_hash": "1ee589d3d728a03561718383bf640171f1aef048573e46fe17239617c7c19fff",
|
||||
"alt": {
|
||||
"hash": "036bb0bcf945115c6488efadcdb56a42192201290a5fe4572ac7b46549146c79",
|
||||
"url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/system-036bb0bcf945115c6488efadcdb56a42192201290a5fe4572ac7b46549146c79.img",
|
||||
"hash": "03397de3da2e9a6d2808b95b72c945b3af71ab79f7f0298624aed62b8749fc5a",
|
||||
"url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/system-03397de3da2e9a6d2808b95b72c945b3af71ab79f7f0298624aed62b8749fc5a.img",
|
||||
"size": 6291456000
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
Xmaq5VKDz8zpg4uJI4cTwgkgKp+HzFlL+5pzHfWvJXqVSNnlOZ/H5dNzm2MScLgzjnxcRYxferbIXjTyZKrzAw==
|
||||
+LyP/p4uBBxbTwRwPrwR/2qPaHkAXmlFkg0GdxOBuujPY89GxP4t5SNKxirqiw9LmMcfA0JfVwMeR3QD9rQ6Dg==
|
||||
|
||||
@@ -67,17 +67,17 @@
|
||||
},
|
||||
{
|
||||
"name": "system",
|
||||
"url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/system-036bb0bcf945115c6488efadcdb56a42192201290a5fe4572ac7b46549146c79.img.xz",
|
||||
"hash": "a0bf6d22e1134fc6c47158d63901c06324ea3b3808ed2fec35801a888cf3d526",
|
||||
"hash_raw": "036bb0bcf945115c6488efadcdb56a42192201290a5fe4572ac7b46549146c79",
|
||||
"url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/system-03397de3da2e9a6d2808b95b72c945b3af71ab79f7f0298624aed62b8749fc5a.img.xz",
|
||||
"hash": "0c9e7dee6c7365600c77b33c4996456459d04aad005e3e41bf6ee2e8af74ceb9",
|
||||
"hash_raw": "03397de3da2e9a6d2808b95b72c945b3af71ab79f7f0298624aed62b8749fc5a",
|
||||
"size": 6291456000,
|
||||
"sparse": true,
|
||||
"full_check": false,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "9a08b97618dceceed48205ec674330bb97c7f76487a6cca2f68288aaa42aaf8e",
|
||||
"ondevice_hash": "1ee589d3d728a03561718383bf640171f1aef048573e46fe17239617c7c19fff",
|
||||
"alt": {
|
||||
"hash": "036bb0bcf945115c6488efadcdb56a42192201290a5fe4572ac7b46549146c79",
|
||||
"url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/system-036bb0bcf945115c6488efadcdb56a42192201290a5fe4572ac7b46549146c79.img",
|
||||
"hash": "03397de3da2e9a6d2808b95b72c945b3af71ab79f7f0298624aed62b8749fc5a",
|
||||
"url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/system-03397de3da2e9a6d2808b95b72c945b3af71ab79f7f0298624aed62b8749fc5a.img",
|
||||
"size": 6291456000
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
IeObYseNfOAYVt6hNKkFMX+RmBAQBU9iiwbox8y6sUdZZs7ZdrUqWJ1ou6yxxsx2JCkgOCQk8z9yT5frz5CeDg==
|
||||
RCYk34Ifjx7S9PIhPm6p49vFMhamPwvw8Lfbb6a6HO8sUOi3J2PcY4VDUcqQZWU6+Am++HP3nJSea8OmAsckAg==
|
||||
|
||||
@@ -69,6 +69,9 @@ NetworkStrength = log.DeviceState.NetworkStrength
|
||||
MM_MODEM_ACCESS_TECHNOLOGY_UMTS = 1 << 5
|
||||
MM_MODEM_ACCESS_TECHNOLOGY_LTE = 1 << 14
|
||||
|
||||
# MMModemStateFailedReason
|
||||
MM_MODEM_STATE_FAILED_REASON_SIM_MISSING = 2
|
||||
|
||||
|
||||
def affine_irq(val, action):
|
||||
irqs = get_irqs_for_action(action)
|
||||
@@ -605,6 +608,32 @@ class Tici(HardwareBase):
|
||||
os.system(f"sudo cp {tf.name} {dest}")
|
||||
os.system(f"sudo nmcli con load {dest}")
|
||||
|
||||
def recover_sim_detection(self) -> bool:
|
||||
# A worn SIM-tray presence switch can read "removed" while the SIM pads still make
|
||||
# contact; with hot-swap detect armed (AT+QSIMDET=1) the modem never powers the SIM
|
||||
# and lands in failed/sim-missing. Disabling detect and rebooting the modem makes it
|
||||
# probe the SIM electrically. Safe to retry on failure: firing disarms the QSIMDET
|
||||
# gate, so a genuinely SIM-less device gets at most one extra modem reboot per boot.
|
||||
if self.get_device_type() not in ("tici", "tizi"):
|
||||
return False
|
||||
|
||||
try:
|
||||
modem = self.get_modem()
|
||||
state = modem.Get(MM_MODEM, 'State', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)
|
||||
if state != MM_MODEM_STATE.FAILED:
|
||||
return False
|
||||
reason = modem.Get(MM_MODEM, 'StateFailedReason', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)
|
||||
if reason != MM_MODEM_STATE_FAILED_REASON_SIM_MISSING:
|
||||
return False
|
||||
detect = str(modem.Command('AT+QSIMDET?', math.ceil(TIMEOUT), dbus_interface=MM_MODEM, timeout=TIMEOUT)).strip()
|
||||
if not detect.startswith('+QSIMDET: 1'):
|
||||
return False
|
||||
modem.Command('AT+QSIMDET=0,0', math.ceil(TIMEOUT), dbus_interface=MM_MODEM, timeout=TIMEOUT)
|
||||
modem.Command('AT+CFUN=1,1', math.ceil(TIMEOUT), dbus_interface=MM_MODEM, timeout=TIMEOUT)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def reboot_modem(self):
|
||||
modem = None
|
||||
try:
|
||||
|
||||
42
system/install_flockd_service.sh
Normal file
42
system/install_flockd_service.sh
Normal file
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/bash
|
||||
set -e
|
||||
|
||||
SERVICE_FILE="/data/openpilot/system/flockd.service"
|
||||
SERVICE_NAME="flockd.service"
|
||||
SERVICE_OVERRIDE="/etc/systemd/system/${SERVICE_NAME}"
|
||||
SERVICE_BAKED="/lib/systemd/system/${SERVICE_NAME}"
|
||||
|
||||
echo "Installing Flock RF detector systemd service..."
|
||||
|
||||
if [ -f "$SERVICE_BAKED" ] && grep -q "/usr/libexec/iqpilot/iqpilot_bundle_runner" "$SERVICE_BAKED"; then
|
||||
echo "Using IQ.OS baked ${SERVICE_NAME}; removing stale override if present..."
|
||||
sudo mount -o remount,rw /
|
||||
sudo rm -f "$SERVICE_OVERRIDE"
|
||||
sudo systemctl daemon-reload
|
||||
sudo mount -o remount,ro /
|
||||
else
|
||||
if [ ! -f "$SERVICE_FILE" ]; then
|
||||
echo "ERROR: Service file not found at $SERVICE_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "IQ.OS baked unit unavailable; installing fallback override into /etc/systemd/system..."
|
||||
sudo cp "$SERVICE_FILE" "$SERVICE_OVERRIDE"
|
||||
sudo systemctl daemon-reload
|
||||
fi
|
||||
|
||||
echo "Enabling $SERVICE_NAME to start at boot..."
|
||||
sudo systemctl enable "$SERVICE_NAME"
|
||||
|
||||
echo "Starting $SERVICE_NAME..."
|
||||
sudo systemctl restart "$SERVICE_NAME"
|
||||
|
||||
echo ""
|
||||
echo "Service status:"
|
||||
sudo systemctl status "$SERVICE_NAME" --no-pager
|
||||
|
||||
echo ""
|
||||
echo "Useful commands:"
|
||||
echo " sudo systemctl status flockd - Check service status"
|
||||
echo " sudo systemctl restart flockd - Restart service"
|
||||
echo " sudo journalctl -u flockd -f - View live logs"
|
||||
@@ -28,6 +28,13 @@ const int SEGMENT_LENGTH = LOGGERD_TEST ? atoi(getenv("LOGGERD_SEGMENT_LENGTH"))
|
||||
constexpr char PRESERVE_ATTR_NAME[] = "user.preserve";
|
||||
constexpr char PRESERVE_ATTR_VALUE = '1';
|
||||
|
||||
// 2.5x the stock 526x330 qcamera, rounded up to even. The msm_vidc encoder rejects
|
||||
// VIDIOC_S_FMT with ENOTSUPP (524) on an odd width or height, which throws out of
|
||||
// encoder_thread and SIGABRTs all of encoderd -- taking fcamera/dcamera/ecamera with it.
|
||||
constexpr int QCAM_WIDTH = 1316;
|
||||
constexpr int QCAM_HEIGHT = 826;
|
||||
static_assert(QCAM_WIDTH % 2 == 0 && QCAM_HEIGHT % 2 == 0, "qcamera dimensions must be even");
|
||||
|
||||
struct EncoderSettings {
|
||||
cereal::EncodeIndex::Type encode_type;
|
||||
int bitrate;
|
||||
@@ -43,10 +50,10 @@ struct EncoderSettings {
|
||||
}
|
||||
|
||||
static EncoderSettings QcamEncoderSettings() {
|
||||
// qcamera.ts is the small "dashcam" copy uploaded to konn3kt. Stock 256kbps @ 526x330 is potato;
|
||||
// bump the bitrate to match the higher resolution below. Still H264/.ts (web/HLS compatible) and
|
||||
// ~1/4 the bitrate of fcamera.hevc, so the file stays small. Override with QCAM_BITRATE if needed.
|
||||
int _qcam_bitrate = getenv("QCAM_BITRATE") ? atoi(getenv("QCAM_BITRATE")) : 1'600'000;
|
||||
// Keep Konn3kt route uploads useful without turning each 60-second qcamera
|
||||
// segment into a 26 MB file. This is still a substantial improvement over
|
||||
// stock qcam, while CBR keeps storage and upload usage predictable.
|
||||
int _qcam_bitrate = getenv("QCAM_BITRATE") ? atoi(getenv("QCAM_BITRATE")) : 2'400'000;
|
||||
return EncoderSettings{.encode_type = cereal::EncodeIndex::Type::QCAMERA_H264, .bitrate = _qcam_bitrate, .gop_size = 15};
|
||||
}
|
||||
|
||||
@@ -140,8 +147,8 @@ const EncoderInfo qcam_encoder_info = {
|
||||
.filename = "qcamera.ts",
|
||||
.cbr = true, // enforce the bitrate so upload size stays predictable (no VBR overshoot)
|
||||
.get_settings = [](int){return EncoderSettings::QcamEncoderSettings();},
|
||||
.frame_width = 1052, // 2x the stock 526x330, same road-cam aspect ratio
|
||||
.frame_height = 660,
|
||||
.frame_width = QCAM_WIDTH,
|
||||
.frame_height = QCAM_HEIGHT,
|
||||
.include_audio = Params().getBool("RecordAudio"),
|
||||
INIT_ENCODE_FUNCTIONS(QRoadEncode),
|
||||
};
|
||||
|
||||
@@ -11,7 +11,6 @@ import threading
|
||||
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
|
||||
def unblock_stdout() -> None:
|
||||
# get a non-blocking stdout
|
||||
@@ -103,51 +102,3 @@ def save_bootlog():
|
||||
t = threading.Thread(target=fn, args=(tmp, ))
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
|
||||
# REVERT ME! ---------------------------------------------------------------
|
||||
# One-shot migration carrying values across the sunny -> IQ param key renames.
|
||||
# Old keys are no longer registered in params_keys.h, so their values are read
|
||||
# straight off disk, copied to the new IQ key, and the stale file removed.
|
||||
# Delete this block (and its manager_init() call) once fielded devices have
|
||||
# booted past it at least once.
|
||||
_RENAMED_PARAMS = {
|
||||
"QuietMode": "IQAlertSilence",
|
||||
"SpeedLimitMode": "IQSpeedAssistMode",
|
||||
"SpeedLimitPolicy": "IQSpeedAssistPolicy",
|
||||
"SpeedLimitOffsetType": "IQSpeedAssistOffsetType",
|
||||
"SpeedLimitValueOffset": "IQSpeedAssistValueOffset",
|
||||
"LaneTurnDesire": "IQLaneTurnDesire",
|
||||
"LaneTurnValue": "IQLaneTurnValue",
|
||||
"BlinkerPauseLateralControl": "IQBlinkerPauseLateral",
|
||||
"BlinkerMinLateralControlSpeed": "IQBlinkerMinLateralSpeed",
|
||||
"DevUIInfo": "IQDevUIInfo",
|
||||
}
|
||||
|
||||
|
||||
def migrate_renamed_params(params: Params | None = None) -> None:
|
||||
"""REVERT ME! carry stored values across the sunny->IQ key renames, then drop the old files.
|
||||
|
||||
Copies at the file level: on-disk param values are raw bytes, so this preserves the exact
|
||||
stored representation and sidesteps put()'s typed-value check for BOOL/INT/FLOAT keys.
|
||||
"""
|
||||
p = params if params is not None else Params()
|
||||
for old, new in _RENAMED_PARAMS.items():
|
||||
try:
|
||||
old_path = p.get_param_path(old)
|
||||
if not os.path.isfile(old_path):
|
||||
continue
|
||||
new_path = p.get_param_path(new)
|
||||
if not os.path.exists(new_path):
|
||||
with open(old_path, "rb") as f:
|
||||
value = f.read()
|
||||
tmp = new_path + ".tmp"
|
||||
with open(tmp, "wb") as f:
|
||||
f.write(value)
|
||||
f.flush()
|
||||
os.fsync(f.fileno())
|
||||
os.rename(tmp, new_path)
|
||||
os.remove(old_path)
|
||||
except Exception:
|
||||
cloudlog.exception(f"param rename migration failed for {old} -> {new}")
|
||||
# END REVERT ME! ------------------------------------------------------------
|
||||
|
||||
@@ -17,7 +17,7 @@ from openpilot.common.params import Params, ParamKeyFlag
|
||||
from openpilot.common.text_window import TextWindow
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
from openpilot.system.loggerd.crash_recovery import recover_unclean_segments
|
||||
from openpilot.system.manager.helpers import unblock_stdout, write_onroad_params, save_bootlog, heal_param_perms, migrate_renamed_params
|
||||
from openpilot.system.manager.helpers import unblock_stdout, write_onroad_params, save_bootlog, heal_param_perms
|
||||
from openpilot.system.manager.process import ensure_running
|
||||
from openpilot.system.manager.process_config import managed_processes
|
||||
from openpilot.iqpilot.konn3kt.registration import register, UNREGISTERED_DONGLE_ID
|
||||
@@ -40,7 +40,6 @@ def manager_init() -> None:
|
||||
build_metadata = get_build_metadata()
|
||||
|
||||
params = Params()
|
||||
migrate_renamed_params(params) # REVERT ME! sunny->IQ param key migration (runs before defaults are filled)
|
||||
params.clear_all(ParamKeyFlag.CLEAR_ON_MANAGER_START)
|
||||
params.clear_all(ParamKeyFlag.CLEAR_ON_ONROAD_TRANSITION)
|
||||
params.clear_all(ParamKeyFlag.CLEAR_ON_OFFROAD_TRANSITION)
|
||||
@@ -50,7 +49,7 @@ def manager_init() -> None:
|
||||
|
||||
# device boot mode
|
||||
if params.get("DeviceBootMode") == 1: # start in Always Offroad mode
|
||||
params.put_bool("OffroadMode", True)
|
||||
params.put_bool("IQAlwaysOffroad", True)
|
||||
|
||||
if params.get_bool("RecordFrontLock"):
|
||||
params.put_bool("RecordFront", True)
|
||||
|
||||
@@ -181,6 +181,32 @@ class NativeProcess(ManagerProcess):
|
||||
self.shutting_down = False
|
||||
|
||||
|
||||
def _normalize_bundle_modes(bundle: str) -> None:
|
||||
import json
|
||||
candidates = []
|
||||
if env_root := os.environ.get("IQPILOT_PROPRIETARY_ROOT"):
|
||||
candidates += [os.path.join(env_root, bundle), env_root]
|
||||
candidates += [
|
||||
os.path.join(BASEDIR, ".iqpilot", "bundles", bundle),
|
||||
os.path.join(os.path.dirname(BASEDIR), ".iqpilot", "bundles", bundle),
|
||||
os.path.join(BASEDIR, "artifacts", bundle),
|
||||
]
|
||||
root = next((c for c in candidates if os.path.isfile(os.path.join(c, "manifest.json"))), None)
|
||||
if root is None:
|
||||
return
|
||||
try:
|
||||
with open(os.path.join(root, "manifest.json")) as f:
|
||||
manifest = json.load(f)
|
||||
for rel, meta in manifest.items():
|
||||
if not (isinstance(meta, dict) and "mode" in meta and "sha256" in meta):
|
||||
continue
|
||||
path = os.path.join(root, rel)
|
||||
if os.path.isfile(path) and (os.stat(path).st_mode & 0o777) != meta["mode"]:
|
||||
os.chmod(path, meta["mode"])
|
||||
except Exception:
|
||||
cloudlog.exception(f"failed to normalize bundle modes for {bundle}")
|
||||
|
||||
|
||||
class BundleProcess(NativeProcess):
|
||||
def __init__(self, name, bundle, entry, should_run, enabled=True, sigkill=False, restart_if_crash=False):
|
||||
self.bundle = bundle
|
||||
@@ -204,6 +230,11 @@ class BundleProcess(NativeProcess):
|
||||
sigkill=sigkill,
|
||||
)
|
||||
|
||||
def start(self) -> None:
|
||||
if self.proc is None:
|
||||
_normalize_bundle_modes(self.bundle)
|
||||
super().start()
|
||||
|
||||
|
||||
class PythonProcess(ManagerProcess):
|
||||
def __init__(self, name, module, should_run, enabled=True, sigkill=False, restart_if_crash=False):
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
"""
|
||||
from openpilot.system.ui.iqpilot.widgets.list_view import IQButtonAction
|
||||
from openpilot.system.ui.iqwidgets.widgets.list_view import IQButtonAction
|
||||
|
||||
|
||||
class NoElideButtonAction(IQButtonAction):
|
||||
class WideButtonAction(IQButtonAction):
|
||||
def get_width_hint(self):
|
||||
return super().get_width_hint() + 1
|
||||
@@ -7,9 +7,9 @@ import time
|
||||
from collections.abc import Callable
|
||||
|
||||
import pyray as rl
|
||||
from openpilot.system.ui.iqpilot.lib.styles import ink, metrics
|
||||
from openpilot.system.ui.iqwidgets.lib.styles import ink, metrics
|
||||
from openpilot.system.ui.widgets.button import Button, ButtonStyle
|
||||
from openpilot.system.ui.iqpilot.lib import canvas
|
||||
from openpilot.system.ui.iqwidgets.lib import canvas
|
||||
from openpilot.system.ui.widgets.scroller_tici import LineSeparator, LINE_COLOR, LINE_PADDING
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.widgets.toggle import Toggle
|
||||
@@ -899,8 +899,8 @@ def progress_item(title):
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.ui.iqpilot.lib.styles import ink
|
||||
from openpilot.system.ui.iqpilot.widgets.helpers.glyphs import draw_star
|
||||
from openpilot.system.ui.iqwidgets.lib.styles import ink
|
||||
from openpilot.system.ui.iqwidgets.widgets.helpers.glyphs import draw_star
|
||||
from openpilot.system.ui.lib.application import FontWeight, gui_app
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
@@ -925,13 +925,13 @@ _FRAME_PAD = 50
|
||||
|
||||
|
||||
@dataclass
|
||||
class TreeNode:
|
||||
class PickerItem:
|
||||
ref: str
|
||||
data: dict = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TreeFolder:
|
||||
class PickerGroup:
|
||||
folder: str
|
||||
nodes: list
|
||||
|
||||
@@ -999,7 +999,7 @@ class _TreeRow(Button):
|
||||
return super()._handle_mouse_release(mouse_pos)
|
||||
|
||||
|
||||
class TreeOptionDialog(MultiOptionDialog):
|
||||
class PickerDialog(MultiOptionDialog):
|
||||
"""Folder/leaf picker with search, favourites, and a pinned current selection."""
|
||||
|
||||
def __init__(self, title, folders, current_ref="", fav_param="", option_font_weight=FontWeight.MEDIUM, search_prompt=None,
|
||||
@@ -1018,7 +1018,7 @@ class TreeOptionDialog(MultiOptionDialog):
|
||||
self.on_exit = on_exit
|
||||
self.display_func = display_func or (lambda node: node.data.get('display_name', node.ref))
|
||||
self.search_funcs = search_funcs or [lambda node: node.data.get('display_name', ''), lambda node: node.data.get('short_name', '')]
|
||||
self.search_title = search_title or tr("Enter search query")
|
||||
self.search_title = search_title or tr("Type to search")
|
||||
self.search_subtitle = search_subtitle
|
||||
self._search_rect: rl.Rectangle | None = None
|
||||
self._search_pressed = False
|
||||
@@ -26,7 +26,7 @@ from openpilot.system.hardware import HARDWARE, PC
|
||||
from openpilot.system.ui.lib.multilang import multilang
|
||||
from openpilot.common.realtime import Ratekeeper
|
||||
|
||||
from openpilot.system.ui.iqpilot.lib.application import IQAppHooks
|
||||
from openpilot.system.ui.iqwidgets.lib.application import IQAppHooks
|
||||
from openpilot.system.ui.lib.screen_recorder import ScreenRecorder
|
||||
|
||||
_DEFAULT_FPS = int(os.getenv("FPS", {'tizi': 20, 'tici': 20}.get(HARDWARE.get_device_type(), 60)))
|
||||
|
||||
@@ -33,10 +33,19 @@ EMOJI_REGEX = re.compile(
|
||||
flags=re.UNICODE
|
||||
)
|
||||
|
||||
_emoji_font_loaded = False
|
||||
|
||||
def _load_emoji_font() -> ImageFont.FreeTypeFont | None:
|
||||
global _emoji_font
|
||||
if _emoji_font is None:
|
||||
_emoji_font = ImageFont.truetype(str(FONT_DIR.joinpath("NotoColorEmoji.ttf")), 109)
|
||||
global _emoji_font, _emoji_font_loaded
|
||||
if not _emoji_font_loaded:
|
||||
_emoji_font_loaded = True
|
||||
try:
|
||||
# FONT_DIR is an importlib.resources path. Inside the setup zipapp it points into the archive,
|
||||
# so str() yields a path through the .zip that PIL can't open ("cannot open resource"). Read
|
||||
# the bytes and hand PIL a file object so it works both on disk and inside the zipapp.
|
||||
_emoji_font = ImageFont.truetype(io.BytesIO(FONT_DIR.joinpath("NotoColorEmoji.ttf").read_bytes()), 109)
|
||||
except Exception:
|
||||
_emoji_font = None # never crash the whole UI over an emoji glyph
|
||||
return _emoji_font
|
||||
|
||||
def find_emoji(text):
|
||||
@@ -44,12 +53,15 @@ def find_emoji(text):
|
||||
|
||||
def emoji_tex(emoji):
|
||||
if emoji not in _cache:
|
||||
font = _load_emoji_font()
|
||||
if font is None:
|
||||
return None
|
||||
img = Image.new("RGBA", (128, 128), (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(img)
|
||||
draw.text((0, 0), emoji, font=_load_emoji_font(), embedded_color=True)
|
||||
draw.text((0, 0), emoji, font=font, embedded_color=True)
|
||||
with io.BytesIO() as buffer:
|
||||
img.save(buffer, format="PNG")
|
||||
l = buffer.tell()
|
||||
buffer.seek(0)
|
||||
_cache[emoji] = rl.load_texture_from_image(rl.load_image_from_memory(".png", buffer.getvalue(), l))
|
||||
return _cache[emoji]
|
||||
return _cache.get(emoji)
|
||||
|
||||
@@ -67,6 +67,9 @@ class MeteredType(IntEnum):
|
||||
NO = 2
|
||||
|
||||
|
||||
_WARNED_UNSUPPORTED_NETWORKS: set[tuple[int, int, int]] = set()
|
||||
|
||||
|
||||
def get_security_type(flags: int, wpa_flags: int, rsn_flags: int) -> SecurityType:
|
||||
wpa_props = wpa_flags | rsn_flags
|
||||
|
||||
@@ -83,7 +86,10 @@ def get_security_type(flags: int, wpa_flags: int, rsn_flags: int) -> SecurityTyp
|
||||
# WPA2, WPA2+WPA3 mixed, or WPA — all handled via WPA key_mgmt (NM negotiates SAE if available)
|
||||
return SecurityType.WPA2
|
||||
else:
|
||||
cloudlog.warning(f"Unsupported network! flags: {flags}, wpa_flags: {wpa_flags}, rsn_flags: {rsn_flags}")
|
||||
_key = (flags, wpa_flags, rsn_flags)
|
||||
if _key not in _WARNED_UNSUPPORTED_NETWORKS:
|
||||
_WARNED_UNSUPPORTED_NETWORKS.add(_key)
|
||||
cloudlog.warning(f"Unsupported network! flags: {flags}, wpa_flags: {wpa_flags}, rsn_flags: {rsn_flags}")
|
||||
return SecurityType.UNSUPPORTED
|
||||
|
||||
|
||||
@@ -630,6 +636,7 @@ class WifiManager:
|
||||
cloudlog.warning("No WiFi device found")
|
||||
return
|
||||
|
||||
self._set_device_autoconnect(True)
|
||||
self._connecting_to_ssid = ssid
|
||||
self._router_main.send(new_method_call(self._nm, 'ActivateConnection', 'ooo',
|
||||
(conn_path, self._wifi_device, "/")))
|
||||
@@ -639,6 +646,37 @@ class WifiManager:
|
||||
else:
|
||||
threading.Thread(target=worker, daemon=True).start()
|
||||
|
||||
def disconnect_connection(self, ssid: str, block: bool = False):
|
||||
def worker():
|
||||
if self._router_main is None:
|
||||
cloudlog.warning(f"WiFi not ready while disconnecting {ssid}")
|
||||
return
|
||||
|
||||
if ssid not in self._get_connections():
|
||||
return
|
||||
|
||||
# the profile stays saved and untouched; without clearing autoconnect on the device
|
||||
# NetworkManager re-associates within seconds
|
||||
self._set_device_autoconnect(False)
|
||||
self._connecting_to_ssid = ""
|
||||
self._deactivate_connection(ssid)
|
||||
self._update_networks()
|
||||
self._enqueue_callbacks(self._disconnected)
|
||||
|
||||
if block:
|
||||
worker()
|
||||
else:
|
||||
threading.Thread(target=worker, daemon=True).start()
|
||||
|
||||
def _set_device_autoconnect(self, enabled: bool) -> None:
|
||||
if self._router_main is None or self._wifi_device is None:
|
||||
return
|
||||
|
||||
dev_addr = DBusAddress(self._wifi_device, bus_name=NM, interface=NM_DEVICE_IFACE)
|
||||
reply = self._router_main.send_and_get_reply(Properties(dev_addr).set('Autoconnect', 'b', enabled))
|
||||
if reply.header.message_type == MessageType.error:
|
||||
cloudlog.warning(f'Failed to set device autoconnect={enabled}: {reply}')
|
||||
|
||||
def _deactivate_connection(self, ssid: str):
|
||||
target_conn_path = self._get_connections().get(ssid, None)
|
||||
if target_conn_path is None:
|
||||
|
||||
@@ -50,6 +50,8 @@ INSTALLER_URL_PATH = "/tmp/installer_url"
|
||||
# "<user>/<branch>" maps to a GitHub fork. IQ.OS uses the DRM "magic" compositor, not Wayland,
|
||||
# so comma's downloaded installers (installer.comma.ai) crash on launch; clone the fork directly.
|
||||
GITHUB_FORK_URL = "https://github.com/{user}/openpilot.git"
|
||||
# IQ.Pilot lives on the IQ Lvbs git server; github.com/IQLvbs is DMCA'd and dead.
|
||||
GIT_URL_OVERRIDES = {"IQLvbs": "https://git.konn3kt.com/IQ.Lvbs/IQ.Pilot.git"}
|
||||
|
||||
CONTINUE = """#!/usr/bin/env bash
|
||||
|
||||
@@ -756,7 +758,7 @@ class Setup(Widget):
|
||||
pass
|
||||
|
||||
def _fork_install_thread(self, user: str, branch: str):
|
||||
git_url = GITHUB_FORK_URL.format(user=user)
|
||||
git_url = GIT_URL_OVERRIDES.get(user) or GITHUB_FORK_URL.format(user=user)
|
||||
label = f"{user}/{branch}"
|
||||
try:
|
||||
subprocess.run(["rm", "-rf", TMP_INSTALL_PATH], check=False)
|
||||
@@ -774,7 +776,8 @@ class Setup(Widget):
|
||||
subprocess.run(["git", "-C", TMP_INSTALL_PATH, "submodule", "update", "--init"], check=False)
|
||||
|
||||
run_cmd(["rm", "-f", VALID_CACHE_PATH])
|
||||
run_cmd(["rm", "-rf", INSTALL_PATH])
|
||||
# sudo: a prior *run* install can leave root-owned .pyc here that a comma-user rm can't delete.
|
||||
run_cmd(["sudo", "rm", "-rf", INSTALL_PATH])
|
||||
run_cmd(["mv", TMP_INSTALL_PATH, INSTALL_PATH])
|
||||
|
||||
self._ble_progress("installing", 90)
|
||||
@@ -882,6 +885,11 @@ class Setup(Widget):
|
||||
# AGNOS might try to execute the installer before this process exits.
|
||||
# Therefore, important to close the fd before renaming the installer.
|
||||
os.close(fd)
|
||||
# comma's ELF installer does `rm -rf /data/openpilot` as the comma user and asserts it
|
||||
# succeeds; a prior *run* install leaves root-owned __pycache__ .pyc it can't delete, so it
|
||||
# aborts before continue.sh and bounces back to setup. Clear the old tree first (sudo) so the
|
||||
# installer's rm succeeds.
|
||||
subprocess.run(["sudo", "rm", "-rf", INSTALL_PATH], check=False)
|
||||
os.rename(tmpfile, INSTALLER_DESTINATION_PATH)
|
||||
|
||||
with open(INSTALLER_URL_PATH, "w") as f:
|
||||
|
||||
@@ -51,6 +51,8 @@ INSTALLER_DESTINATION_PATH = "/tmp/installer"
|
||||
INSTALLER_URL_PATH = "/tmp/installer_url"
|
||||
|
||||
GITHUB_FORK_URL = "https://github.com/{user}/openpilot.git"
|
||||
# IQ.Pilot lives on the IQ Lvbs git server; github.com/IQLvbs is DMCA'd and dead.
|
||||
GIT_URL_OVERRIDES = {"IQLvbs": "https://git.konn3kt.com/IQ.Lvbs/IQ.Pilot.git"}
|
||||
|
||||
CONTINUE = """#!/usr/bin/env bash
|
||||
|
||||
@@ -589,7 +591,7 @@ class Setup(Widget):
|
||||
pass
|
||||
|
||||
def _fork_install_thread(self, user: str, branch: str):
|
||||
git_url = GITHUB_FORK_URL.format(user=user)
|
||||
git_url = GIT_URL_OVERRIDES.get(user) or GITHUB_FORK_URL.format(user=user)
|
||||
label = f"{user}/{branch}"
|
||||
fail_msg = "Ensure the entered URL is valid, and the device's internet connection is good."
|
||||
try:
|
||||
@@ -609,7 +611,8 @@ class Setup(Widget):
|
||||
subprocess.run(["git", "-C", TMP_INSTALL_PATH, "submodule", "update", "--init"], check=False)
|
||||
|
||||
run_cmd(["rm", "-f", VALID_CACHE_PATH])
|
||||
run_cmd(["rm", "-rf", INSTALL_PATH])
|
||||
# sudo: a prior *run* install can leave root-owned .pyc here that a comma-user rm can't delete.
|
||||
run_cmd(["sudo", "rm", "-rf", INSTALL_PATH])
|
||||
run_cmd(["mv", TMP_INSTALL_PATH, INSTALL_PATH])
|
||||
|
||||
self._ble_progress("installing", 90)
|
||||
@@ -718,6 +721,11 @@ class Setup(Widget):
|
||||
# AGNOS might try to execute the installer before this process exits.
|
||||
# Therefore, important to close the fd before renaming the installer.
|
||||
os.close(fd)
|
||||
# comma's ELF installer does `rm -rf /data/openpilot` as the comma user and asserts it
|
||||
# succeeds; a prior *run* install leaves root-owned __pycache__ .pyc it can't delete, so it
|
||||
# aborts before continue.sh and bounces back to setup. Clear the old tree first (sudo) so the
|
||||
# installer's rm succeeds.
|
||||
subprocess.run(["sudo", "rm", "-rf", INSTALL_PATH], check=False)
|
||||
os.rename(tmpfile, INSTALLER_DESTINATION_PATH)
|
||||
|
||||
with open(INSTALLER_URL_PATH, "w") as f:
|
||||
|
||||
@@ -406,7 +406,8 @@ class Label(Widget):
|
||||
line_pos.x += width_before.x
|
||||
|
||||
tex = emoji_tex(emoji)
|
||||
rl.draw_texture_ex(tex, line_pos, 0.0, self._font_size / tex.height * FONT_SCALE, self._text_color)
|
||||
if tex is not None:
|
||||
rl.draw_texture_ex(tex, line_pos, 0.0, self._font_size / tex.height * FONT_SCALE, self._text_color)
|
||||
line_pos.x += self._font_size * FONT_SCALE
|
||||
prev_index = end
|
||||
rl.draw_text_ex(self._font, text[prev_index:], line_pos, self._font_size, 0, self._text_color)
|
||||
@@ -849,8 +850,9 @@ class UnifiedLabel(Widget):
|
||||
|
||||
# Draw emoji
|
||||
tex = emoji_tex(emoji)
|
||||
emoji_scale = self._font_size / tex.height * FONT_SCALE
|
||||
rl.draw_texture_ex(tex, line_pos, 0.0, emoji_scale, self._text_color)
|
||||
if tex is not None:
|
||||
emoji_scale = self._font_size / tex.height * FONT_SCALE
|
||||
rl.draw_texture_ex(tex, line_pos, 0.0, emoji_scale, self._text_color)
|
||||
# Emoji width is font_size * FONT_SCALE (as per measure_text_cached)
|
||||
line_pos.x += self._font_size * FONT_SCALE
|
||||
prev_index = end
|
||||
|
||||
@@ -17,10 +17,10 @@ from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
from openpilot.system.ui.widgets.list_view import ButtonAction, ListItem, MultipleButtonAction, ToggleAction, button_item, text_item
|
||||
|
||||
if gui_app.iqpilot_ui():
|
||||
from openpilot.system.ui.iqpilot.widgets.list_view import button_item
|
||||
from openpilot.system.ui.iqpilot.widgets.list_view import IQListItem as ListItem
|
||||
from openpilot.system.ui.iqpilot.widgets.list_view import IQToggleAction as ToggleAction
|
||||
from openpilot.system.ui.iqpilot.widgets.list_view import IQMultipleButtonAction as MultipleButtonAction
|
||||
from openpilot.system.ui.iqwidgets.widgets.list_view import button_item
|
||||
from openpilot.system.ui.iqwidgets.widgets.list_view import IQListItem as ListItem
|
||||
from openpilot.system.ui.iqwidgets.widgets.list_view import IQToggleAction as ToggleAction
|
||||
from openpilot.system.ui.iqwidgets.widgets.list_view import IQMultipleButtonAction as MultipleButtonAction
|
||||
|
||||
# These are only used for AdvancedNetworkSettings, standalone apps just need WifiManagerUI
|
||||
try:
|
||||
@@ -66,6 +66,7 @@ class UIState(IntEnum):
|
||||
NEEDS_AUTH = 2
|
||||
SHOW_FORGET_CONFIRM = 3
|
||||
FORGETTING = 4
|
||||
DISCONNECTING = 5
|
||||
|
||||
|
||||
class NavButton(Widget):
|
||||
@@ -328,6 +329,7 @@ class WifiManagerUI(Widget):
|
||||
self._state_network: Network | None = None # for CONNECTING / NEEDS_AUTH / SHOW_FORGET_CONFIRM / FORGETTING
|
||||
self._password_retry: bool = False # for NEEDS_AUTH
|
||||
self.btn_width: int = 200
|
||||
self.disconnect_btn_width: int = 300
|
||||
self.scroll_panel = GuiScrollPanel()
|
||||
self.keyboard = Keyboard(max_text_size=MAX_PASSWORD_LENGTH, min_text_size=MIN_PASSWORD_LENGTH, show_password_toggle=True)
|
||||
self._load_icons()
|
||||
@@ -335,6 +337,7 @@ class WifiManagerUI(Widget):
|
||||
self._networks: list[Network] = []
|
||||
self._networks_buttons: dict[str, Button] = {}
|
||||
self._forget_networks_buttons: dict[str, Button] = {}
|
||||
self._disconnect_networks_buttons: dict[str, Button] = {}
|
||||
|
||||
self._wifi_manager.add_callbacks(need_auth=self._on_need_auth,
|
||||
activated=self._on_activated,
|
||||
@@ -417,7 +420,9 @@ class WifiManagerUI(Widget):
|
||||
|
||||
def _draw_network_item(self, rect, network: Network):
|
||||
spacing = 50
|
||||
ssid_rect = rl.Rectangle(rect.x, rect.y, rect.width - self.btn_width * 2, ITEM_HEIGHT)
|
||||
show_disconnect = network.is_connected and self.state not in (UIState.CONNECTING, UIState.DISCONNECTING)
|
||||
reserved = self.btn_width * 2 + (self.disconnect_btn_width + spacing if show_disconnect else 0)
|
||||
ssid_rect = rl.Rectangle(rect.x, rect.y, rect.width - reserved, ITEM_HEIGHT)
|
||||
signal_icon_rect = rl.Rectangle(rect.x + rect.width - ICON_SIZE, rect.y + (ITEM_HEIGHT - ICON_SIZE) / 2, ICON_SIZE, ICON_SIZE)
|
||||
security_icon_rect = rl.Rectangle(signal_icon_rect.x - spacing - ICON_SIZE, rect.y + (ITEM_HEIGHT - ICON_SIZE) / 2, ICON_SIZE, ICON_SIZE)
|
||||
|
||||
@@ -434,6 +439,10 @@ class WifiManagerUI(Widget):
|
||||
if self._state_network.ssid == network.ssid:
|
||||
self._networks_buttons[network.ssid].set_enabled(False)
|
||||
status_text = tr("FORGETTING...")
|
||||
elif self.state == UIState.DISCONNECTING and self._state_network:
|
||||
if self._state_network.ssid == network.ssid:
|
||||
self._networks_buttons[network.ssid].set_enabled(False)
|
||||
status_text = tr("DISCONNECTING...")
|
||||
elif network.security_type == SecurityType.UNSUPPORTED:
|
||||
self._networks_buttons[network.ssid].set_enabled(False)
|
||||
else:
|
||||
@@ -455,6 +464,15 @@ class WifiManagerUI(Widget):
|
||||
)
|
||||
self._forget_networks_buttons[network.ssid].render(forget_btn_rect)
|
||||
|
||||
if show_disconnect:
|
||||
disconnect_btn_rect = rl.Rectangle(
|
||||
forget_btn_rect.x - self.btn_width - spacing,
|
||||
forget_btn_rect.y,
|
||||
self.btn_width,
|
||||
80,
|
||||
)
|
||||
self._disconnect_networks_buttons[network.ssid].render(disconnect_btn_rect)
|
||||
|
||||
self._draw_status_icon(security_icon_rect, network)
|
||||
self._draw_signal_strength_icon(signal_icon_rect, network)
|
||||
|
||||
@@ -470,6 +488,9 @@ class WifiManagerUI(Widget):
|
||||
self.state = UIState.SHOW_FORGET_CONFIRM
|
||||
self._state_network = network
|
||||
|
||||
def _disconnect_networks_buttons_callback(self, network):
|
||||
self.disconnect_network(network)
|
||||
|
||||
def _draw_status_icon(self, rect, network: Network):
|
||||
"""Draw the status icon based on network's connection state"""
|
||||
icon_file = None
|
||||
@@ -506,6 +527,11 @@ class WifiManagerUI(Widget):
|
||||
self._state_network = network
|
||||
self._wifi_manager.forget_connection(network.ssid)
|
||||
|
||||
def disconnect_network(self, network: Network):
|
||||
self.state = UIState.DISCONNECTING
|
||||
self._state_network = network
|
||||
self._wifi_manager.disconnect_connection(network.ssid)
|
||||
|
||||
def _on_network_updated(self, networks: list[Network]):
|
||||
self._networks = networks
|
||||
for n in self._networks:
|
||||
@@ -515,6 +541,9 @@ class WifiManagerUI(Widget):
|
||||
self._forget_networks_buttons[n.ssid] = Button(tr("Forget"), partial(self._forget_networks_buttons_callback, n), button_style=ButtonStyle.FORGET_WIFI,
|
||||
font_size=45)
|
||||
self._forget_networks_buttons[n.ssid].set_touch_valid_callback(lambda: self.scroll_panel.is_touch_valid())
|
||||
self._disconnect_networks_buttons[n.ssid] = Button(tr("Disconnect"), partial(self._disconnect_networks_buttons_callback, n),
|
||||
button_style=ButtonStyle.FORGET_WIFI, font_size=45)
|
||||
self._disconnect_networks_buttons[n.ssid].set_touch_valid_callback(lambda: self.scroll_panel.is_touch_valid())
|
||||
|
||||
def _on_need_auth(self, ssid):
|
||||
network = next((n for n in self._networks if n.ssid == ssid), None)
|
||||
@@ -532,7 +561,7 @@ class WifiManagerUI(Widget):
|
||||
self.state = UIState.IDLE
|
||||
|
||||
def _on_disconnected(self):
|
||||
if self.state == UIState.CONNECTING:
|
||||
if self.state in (UIState.CONNECTING, UIState.DISCONNECTING):
|
||||
self.state = UIState.IDLE
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user