IQ.Pilot Release Commit @ 2f37564
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")
|
||||
@@ -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-756453317d208f873d44e14e9429d466c90cdcee70de52fede0bfdd6f6571c7b.img.xz",
|
||||
"hash": "654de83dd64190701c5a9dc7297fb569ba106cc63217725893920df6b8c9eaa0",
|
||||
"hash_raw": "756453317d208f873d44e14e9429d466c90cdcee70de52fede0bfdd6f6571c7b",
|
||||
"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": "8269bd017b18a1a4fd5e093b96efa181fd803e200fa92364fe9307f25445f596",
|
||||
"ondevice_hash": "1ee589d3d728a03561718383bf640171f1aef048573e46fe17239617c7c19fff",
|
||||
"alt": {
|
||||
"hash": "756453317d208f873d44e14e9429d466c90cdcee70de52fede0bfdd6f6571c7b",
|
||||
"url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/system-756453317d208f873d44e14e9429d466c90cdcee70de52fede0bfdd6f6571c7b.img",
|
||||
"hash": "03397de3da2e9a6d2808b95b72c945b3af71ab79f7f0298624aed62b8749fc5a",
|
||||
"url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/system-03397de3da2e9a6d2808b95b72c945b3af71ab79f7f0298624aed62b8749fc5a.img",
|
||||
"size": 6291456000
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
6ZRxEduDaLm1coUVvI/0QAZtmbf+tY0TWMZtaEaxV7I2fKFfGPG/6oOizuFsmd/SSQls84kap9MwKDEc8XfIDA==
|
||||
+LyP/p4uBBxbTwRwPrwR/2qPaHkAXmlFkg0GdxOBuujPY89GxP4t5SNKxirqiw9LmMcfA0JfVwMeR3QD9rQ6Dg==
|
||||
|
||||
@@ -67,17 +67,17 @@
|
||||
},
|
||||
{
|
||||
"name": "system",
|
||||
"url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/system-756453317d208f873d44e14e9429d466c90cdcee70de52fede0bfdd6f6571c7b.img.xz",
|
||||
"hash": "654de83dd64190701c5a9dc7297fb569ba106cc63217725893920df6b8c9eaa0",
|
||||
"hash_raw": "756453317d208f873d44e14e9429d466c90cdcee70de52fede0bfdd6f6571c7b",
|
||||
"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": "8269bd017b18a1a4fd5e093b96efa181fd803e200fa92364fe9307f25445f596",
|
||||
"ondevice_hash": "1ee589d3d728a03561718383bf640171f1aef048573e46fe17239617c7c19fff",
|
||||
"alt": {
|
||||
"hash": "756453317d208f873d44e14e9429d466c90cdcee70de52fede0bfdd6f6571c7b",
|
||||
"url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/system-756453317d208f873d44e14e9429d466c90cdcee70de52fede0bfdd6f6571c7b.img",
|
||||
"hash": "03397de3da2e9a6d2808b95b72c945b3af71ab79f7f0298624aed62b8749fc5a",
|
||||
"url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/system-03397de3da2e9a6d2808b95b72c945b3af71ab79f7f0298624aed62b8749fc5a.img",
|
||||
"size": 6291456000
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
+gqhJbKMr9i9lW7WrtKebqKXlM0Asr/i7ZodOzIAHzAgzGEvPCWbfco2jo0/jO7O2UzZwKJUl/WzHlsOtJvnCw==
|
||||
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"
|
||||
@@ -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)))
|
||||
|
||||
@@ -636,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, "/")))
|
||||
@@ -645,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:
|
||||
|
||||
@@ -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