diff --git a/openpilot/starpilot/galaxy/starport_send.py b/openpilot/starpilot/galaxy/starport_send.py
index 2be1c6d37d..91e668a32c 100644
--- a/openpilot/starpilot/galaxy/starport_send.py
+++ b/openpilot/starpilot/galaxy/starport_send.py
@@ -11,15 +11,14 @@ from __future__ import annotations
import base64
import hashlib
import json
-import os
import re
-import tempfile
import threading
import time
from collections.abc import Callable
from pathlib import Path
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
+from openpilot.common.utils import atomic_write
from openpilot.starpilot.system.starport import identity as identity_store
MESSAGE_LIMIT = 60
@@ -148,15 +147,8 @@ def _response_json(response) -> dict:
def _save_thread_id(path: Path, key: str, thread_id: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
# A power loss during a write must not truncate the previous cache entry.
- with tempfile.NamedTemporaryFile(mode="w", dir=path.parent, delete=False) as temporary:
- temporary_path = Path(temporary.name)
- try:
- json.dump({"key": key, "thread_id": thread_id}, temporary)
- temporary.flush()
- os.fsync(temporary.fileno())
- os.replace(temporary_path, path)
- finally:
- temporary_path.unlink(missing_ok=True)
+ with atomic_write(str(path), overwrite=True) as handle:
+ json.dump({"key": key, "thread_id": thread_id}, handle)
def send(data: bytes, name: str, note: str, summary: dict, url: str | None = None, post=None,
diff --git a/openpilot/starpilot/galaxy/tests/test_starport.mjs b/openpilot/starpilot/galaxy/tests/test_starport.mjs
index f2ef23f936..a29a6b9c7b 100644
--- a/openpilot/starpilot/galaxy/tests/test_starport.mjs
+++ b/openpilot/starpilot/galaxy/tests/test_starport.mjs
@@ -17,7 +17,7 @@ assert.equal(validPromptInput({ kind: "pin" }, "1234"), true)
assert.equal(validPromptInput({ kind: "pin" }, "\n"), false)
assert.equal(validPromptInput({ kind: "passkey" }, "1234567"), false)
assert.equal(validPromptInput(prompt, ""), true)
-assert.match(StarPortPage.template, /Check that this code matches your car/)
+assert.match(StarPortPage.template, /Check that this code matches your/)
assert.match(StarPortPage.template, /Cancel Search \/ Pairing/)
assert.doesNotMatch(StarPortPage.template, /:row="wiredRow"/)
assert.match(StarPortPage.template, /Read the Android Auto troubleshooting guide/)
@@ -484,6 +484,9 @@ typed.runtime = { car_type: 'adapter' }
assert.equal(typed.adapterSetup, true)
assert.equal(typed.pairsHere, true)
assert.equal(typed.findLabel, 'adapter')
+assert.equal(typed.deviceWord, 'adapter', 'the pairing prompts name the adapter, not the car')
+typed.runtime = { car_type: 'builtin' }
+assert.equal(typed.deviceWord, 'car')
typed.runtime = { car_type: 'usb' }
assert.equal(typed.pairsHere, false, 'a USB cable needs no Bluetooth pairing')
typed.runtime = { running: false, connection: 'wired' }
diff --git a/openpilot/starpilot/galaxy/web/css/galaxy.css b/openpilot/starpilot/galaxy/web/css/galaxy.css
index 63bbb1f223..a995d57cdd 100644
--- a/openpilot/starpilot/galaxy/web/css/galaxy.css
+++ b/openpilot/starpilot/galaxy/web/css/galaxy.css
@@ -2039,7 +2039,6 @@ progress { max-width: 100%; accent-color: var(--primary); }
.gx-guided__step-label { font-size: 13px; font-weight: var(--fw-bold); color: var(--g-muted); }
.gx-guided__step.is-current .gx-guided__step-label { color: var(--g-ink); }
@keyframes gx-guided-pop { 0% { transform: scale(.7); } 60% { transform: scale(1.12); } 100% { transform: scale(1); } }
-.gx-guided__sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.gx-guided__header { display: grid; gap: 6px; max-width: 34rem; }
.gx-guided__title { margin: 0; font-size: 30px; line-height: 1.12; font-weight: var(--fw-bold); letter-spacing: -.02em; text-wrap: balance; }
diff --git a/openpilot/starpilot/galaxy/web/js/starport-guided.js b/openpilot/starpilot/galaxy/web/js/starport-guided.js
index f703c348f6..ade56ed1e6 100644
--- a/openpilot/starpilot/galaxy/web/js/starport-guided.js
+++ b/openpilot/starpilot/galaxy/web/js/starport-guided.js
@@ -134,7 +134,7 @@ export const StarPortGuidedSetup = {
{{ index + 1 }}
- {{ step.label }}{{ step.state === 'done' ? ', done' : step.state === 'current' ? ', current step' : '' }}
+ {{ step.label }}{{ step.state === 'done' ? ', done' : step.state === 'current' ? ', current step' : '' }}
diff --git a/openpilot/starpilot/system/starport/bootstrap.py b/openpilot/starpilot/system/starport/bootstrap.py
index 9e23f0d0c4..2836bb6089 100644
--- a/openpilot/starpilot/system/starport/bootstrap.py
+++ b/openpilot/starpilot/system/starport/bootstrap.py
@@ -275,7 +275,6 @@ class WirelessBootstrap:
self.patience = patience # stage timeouts a car that keeps saying "not ready" gets (setup ladder)
self.channel_override = channel_override # Wi-Fi band to answer with inside the phone-style reply; None keeps the car's own
self.version_major = 0
- self.car_started = False
self.flags = EXPERIMENTS[self.experiment][1] if self.experiment else set()
self.initial_kick_delay = initial_kick_delay
self.join_ping_interval = join_ping_interval
@@ -347,11 +346,6 @@ class WirelessBootstrap:
self.log("bootstrap_version_reply", style="modern", channel_type=selected, hex=reply.hex())
return reply
- def silent_after_modern_version(self, error: BaseException) -> bool:
- """A v4+ head unit took our version reply but never started; the other reply style may suit it."""
- return (isinstance(error, BootstrapTimeout) and error.stage == "wifi_start" and self.version_major >= MODERN_MAJOR
- and not self.car_started)
-
def service(self, message_id: int, payload: bytes) -> bool:
"""Handle stage-independent frames; returns True when consumed."""
if message_id == WIFI_PING_REQUEST:
@@ -423,7 +417,6 @@ class WirelessBootstrap:
if start_request_at is None:
start_request_at = time.monotonic() + self.start_request_delay
elif message_id == WIFI_START_REQUEST:
- self.car_started = True
parsed = parse_endpoint(payload)
if parsed is None:
raise BootstrapError(self.stage, "WifiStartRequest has no valid IPv4 endpoint")
@@ -431,7 +424,6 @@ class WirelessBootstrap:
if "start_response_first" in self.flags:
self.send(WIFI_START_RESPONSE, field(3, STATUS_SUCCESS))
elif message_id == WIFI_START_RESPONSE:
- self.car_started = True
# The car's answer to our WifiStartRequest (2025 Honda): ip=1, port=2, status=3 once it is
# ready, or a negative status alone before then (it may follow up on its own; ask again anyway).
status = signed(one(parse_fields(payload), 3, STATUS_SUCCESS))
diff --git a/openpilot/starpilot/system/starport/bugsink_diagnosis.py b/openpilot/starpilot/system/starport/bugsink_diagnosis.py
index f9a3d97dae..b7630d0187 100644
--- a/openpilot/starpilot/system/starport/bugsink_diagnosis.py
+++ b/openpilot/starpilot/system/starport/bugsink_diagnosis.py
@@ -99,7 +99,7 @@ def attempt_records(events):
STREAM_LOSS = {
"shared_radio": ("comma's own Wi-Fi", "The car stopped acknowledging video while the comma's own Wi-Fi joined or roamed; both share one radio.",
- ["Check the other_wifi_changed and ack_grace events for the network and how long acks stopped.",
+ ["Check the other_wifi_changed and ack_grace events for when it changed and how long acks stopped.",
"Repeated drops at home or on a hotspot mean the 8 s grace is too short or the car takes its screen back first."]),
"stalled": ("Video acknowledgement older than", "The car kept the link but stopped acknowledging video for too long, so the stream was ended.",
["Compare the Wi-Fi signal, frame age and encoder recoveries around the stall.",
diff --git a/openpilot/starpilot/system/starport/network.py b/openpilot/starpilot/system/starport/network.py
index 9609b64492..1611016419 100644
--- a/openpilot/starpilot/system/starport/network.py
+++ b/openpilot/starpilot/system/starport/network.py
@@ -109,37 +109,6 @@ def recover_autoconnect(log: Callable[..., None], marker: Path = AUTOCONNECT_MAR
return True
-SETTINGS_PROFILE_PREFIX = "openpilot connection " # system/ui/lib/wifi_manager.py names saved networks this way
-
-
-def profile_network_name(profile: str) -> str:
- """A saved connection's network name: "openpilot connection Home" -> "Home"; other profiles as they are."""
- return profile[len(SETTINGS_PROFILE_PREFIX):] if profile.startswith(SETTINGS_PROFILE_PREFIX) else profile
-
-
-def wifi_network_name(interface: str = "wlan0") -> str:
- """The network the comma's own Wi-Fi is on or joining, for telling the driver; "" if none or unknown.
-
- Its own D-Bus connection: never the car lease's, whose lock the link check and reconnect wait on."""
- lease = NetworkLease(lambda *args, **kwargs: None, interface)
- try:
- for path, name in lease._wifi_devices():
- if name != interface:
- continue
- access_point = lease._get(path, NM_WIRELESS_IFACE, "ActiveAccessPoint")
- if access_point and access_point != "/":
- ssid = bytes(lease._get(access_point, NM_AP_IFACE, "Ssid")).decode("utf-8", "replace").strip()
- if ssid:
- return ssid
- # Still joining (no access point yet): the profile's name, which the comma's Wi-Fi settings prefix.
- active = lease._get(path, NM_DEVICE_IFACE, "ActiveConnection")
- return profile_network_name(str(lease._get(active, NM_ACTIVE_IFACE, "Id"))) if active and active != "/" else ""
- return ""
- finally:
- if lease.router is not None:
- lease.router.close()
-
-
def connection_settings(credentials: WifiCredentials, interface: str) -> dict:
wireless = {
"ssid": ("ay", credentials.ssid.encode("utf-8")),
diff --git a/openpilot/starpilot/system/starport/new_setup.py b/openpilot/starpilot/system/starport/new_setup.py
index 49708eecb1..6cca56c184 100644
--- a/openpilot/starpilot/system/starport/new_setup.py
+++ b/openpilot/starpilot/system/starport/new_setup.py
@@ -5,6 +5,7 @@ read and change with the StarPort service down or broken, and it needs no device
"""
from pathlib import Path
+from openpilot.common.utils import atomic_write
from openpilot.starpilot.system.starport.identity import DATA_DIR
FLAG_PATH = DATA_DIR / "new_setup"
@@ -22,9 +23,8 @@ def set_enabled(value: bool, path: Path | None = None) -> bool:
target = path or FLAG_PATH
try:
target.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
- temporary = target.with_name(f".{target.name}.tmp")
- temporary.write_text("1" if value else "0")
- temporary.replace(target)
+ with atomic_write(str(target), overwrite=True) as handle:
+ handle.write("1" if value else "0")
except OSError:
return False
return enabled(target) == value
diff --git a/openpilot/starpilot/system/starport/reconnect_notice.py b/openpilot/starpilot/system/starport/reconnect_notice.py
index 9862e9e66d..0cf7a42792 100644
--- a/openpilot/starpilot/system/starport/reconnect_notice.py
+++ b/openpilot/starpilot/system/starport/reconnect_notice.py
@@ -3,31 +3,22 @@
from __future__ import annotations
import json
-import os
import time
from pathlib import Path
+from openpilot.common.utils import atomic_write
from openpilot.starpilot.system.starport import identity as identity_store
NOTICE_PATH = identity_store.DATA_DIR / "reconnect_notice.json"
NOTICE_FRESH_S = 120.0 # a notice older than this is about an earlier drive or reconnect (monotonic: same boot only)
-NAME_MAX = 24
-
-
-def shared_radio_text(network: str) -> str:
- """The car-screen line for a drop caused by the comma's own Wi-Fi joining or roaming."""
- name = network.strip()
- if len(name) > NAME_MAX:
- name = name[:NAME_MAX - 3].rstrip() + "..." # plain dots: the car-screen fonts may lack an ellipsis glyph
- return f"Reconnected: comma Wi-Fi Joined {name}" if name else "Reconnected: comma Wi-Fi Was Switching Networks"
+SHARED_RADIO_TEXT = "Reconnected: comma Wi-Fi Was Switching Networks" # on the car screen after a drop caused by the comma's own Wi-Fi
def write(text: str, path: Path = NOTICE_PATH, now: float | None = None) -> None:
try:
path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
- temporary = path.with_suffix(".tmp")
- temporary.write_text(json.dumps({"text": text, "written": time.monotonic() if now is None else now}))
- os.replace(temporary, path)
+ with atomic_write(str(path), overwrite=True) as handle:
+ json.dump({"text": text, "written": time.monotonic() if now is None else now}, handle)
except OSError:
pass # the notice is a courtesy; projection never depends on it
diff --git a/openpilot/starpilot/system/starport/session.py b/openpilot/starpilot/system/starport/session.py
index f7d19ff8f9..f577caca8f 100644
--- a/openpilot/starpilot/system/starport/session.py
+++ b/openpilot/starpilot/system/starport/session.py
@@ -118,12 +118,6 @@ class PeerRequestedStop(EOFError):
class SharedRadioStall(TimeoutError):
"""The car stopped acknowledging video while the comma's other Wi-Fi network was joining or roaming."""
-
- def __init__(self, message: str, network: str):
- super().__init__(message)
- self.network = network
-
-
START_CODE = re.compile(b"\x00\x00(?:\x00)?\x01")
@@ -659,12 +653,10 @@ class ProjectionSession(Session):
MAX_WINDOW = 2
nav = None
grace_frame: float | None = None # send time of the frame whose late ack is being waited out
- grace_network = ""
grace_focus_lost = False
- # Replaced by the supervisor: the comma's other Wi-Fi network's name if it joined or roamed in the last few
- # seconds ("" when unnamed), else None.
- radio_change: Callable[..., str | None] = staticmethod(lambda: None)
+ # Replaced by the supervisor: whether the comma's other Wi-Fi joined or roamed in the last few seconds.
+ radio_change: Callable[..., bool] = staticmethod(lambda: False)
# HeadunitPad acknowledges every fragment (codec config included) rather than every frame.
fragment_acks = False
@@ -965,7 +957,7 @@ class ProjectionSession(Session):
self.acked += count
self.epoch_acked += count
if self.grace_frame is not None and (not self.pending or self.pending[0] != self.grace_frame):
- self.event("ack_grace_recovered", network=self.grace_network, gap_ms=round((now - self.grace_frame) * 1000))
+ self.event("ack_grace_recovered", gap_ms=round((now - self.grace_frame) * 1000))
self.grace_frame = None
self._confirm_video()
@@ -991,7 +983,7 @@ class ProjectionSession(Session):
measured = now is None
now = time.monotonic() if measured else now
if self.grace_focus_lost:
- raise SharedRadioStall("The car took its screen back while the comma's Wi-Fi was switching networks", self.grace_network)
+ raise SharedRadioStall("The car took its screen back while the comma's Wi-Fi was switching networks")
if self._ack_overdue(now):
# ACKs that arrived while this loop was busy (a slow encode, a CPU burst) still sit in the socket; the
# streaming loop drains only a bounded batch per pass, so read what is there before calling the car silent.
@@ -1010,18 +1002,13 @@ class ProjectionSession(Session):
def _wait_out_radio_change(self, now: float) -> None:
"""The oldest frame is past ACK_TIMEOUT: give up, unless the comma's other Wi-Fi just changed networks."""
oldest = self.pending[0]
- if self.grace_frame == oldest:
- network = self.grace_network
- else:
- changed = self.radio_change()
- if changed is None:
+ if self.grace_frame != oldest:
+ if not self.radio_change():
raise TimeoutError(f"Video acknowledgement older than {self.ACK_TIMEOUT:.1f} s")
- self.grace_frame, self.grace_network = oldest, changed
- self.event("ack_grace", network=changed, after_ms=round((now - oldest) * 1000))
- network = changed
+ self.grace_frame = oldest
+ self.event("ack_grace", after_ms=round((now - oldest) * 1000))
if now - oldest > self.ACK_GRACE_TIMEOUT:
- message = f"Video acknowledgement older than {self.ACK_GRACE_TIMEOUT:.1f} s while the comma's Wi-Fi was switching networks"
- raise SharedRadioStall(message, network)
+ raise SharedRadioStall(f"Video acknowledgement older than {self.ACK_GRACE_TIMEOUT:.1f} s while the comma's Wi-Fi was switching networks")
def send_frame(self, data: bytes, timestamp_us: int, *, keyframe: bool) -> None:
if not self.can_send():
diff --git a/openpilot/starpilot/system/starport/setup_ladder.py b/openpilot/starpilot/system/starport/setup_ladder.py
index 11ab9c6a1a..40b5187b2c 100644
--- a/openpilot/starpilot/system/starport/setup_ladder.py
+++ b/openpilot/starpilot/system/starport/setup_ladder.py
@@ -15,12 +15,11 @@ The first pass for a car is exactly what ran before (rung ``standard``); the lat
from __future__ import annotations
import json
-import os
-import tempfile
import time
from dataclasses import dataclass
from pathlib import Path
+from openpilot.common.utils import atomic_write
from openpilot.starpilot.system.starport.bootstrap import CHANNEL_24GHZ, CHANNEL_DUAL, MODERN_MAJOR, REFUSAL_PATIENCE
from openpilot.starpilot.system.starport.identity import DATA_DIR
@@ -152,17 +151,8 @@ class SetupLadder:
self.cars = ordered
try:
self.path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
- fd, temporary = tempfile.mkstemp(dir=self.path.parent, prefix=".setup-state-")
- try:
- with os.fdopen(fd, "w") as handle:
- json.dump({"version": STATE_VERSION, "cars": ordered}, handle, separators=(",", ":"))
- handle.flush()
- os.fsync(handle.fileno())
- os.chmod(temporary, 0o600)
- os.replace(temporary, self.path)
- finally:
- if os.path.exists(temporary):
- os.unlink(temporary)
+ with atomic_write(str(self.path), overwrite=True) as handle:
+ json.dump({"version": STATE_VERSION, "cars": ordered}, handle, separators=(",", ":"))
except OSError:
return False # progress is a convenience; a read-only disk must never stop a connection
return True
diff --git a/openpilot/starpilot/system/starport/supervisor.py b/openpilot/starpilot/system/starport/supervisor.py
index 90dbe7a196..2ba3b6da0b 100644
--- a/openpilot/starpilot/system/starport/supervisor.py
+++ b/openpilot/starpilot/system/starport/supervisor.py
@@ -258,19 +258,14 @@ class LinkWatch:
class SharedRadioWatch:
"""Notices the comma's own Wi-Fi joining or roaming while the car is on aa0; both share one radio.
- The link going down or up bumps carrier_changes, a plain sysfs read. The network's name comes from
- NetworkManager on this thread, never the streaming one.
+ The link going down or up bumps carrier_changes, a plain sysfs read. The network's name is never read or logged.
"""
def __init__(self, log: Callable[..., None], interface: str = "wlan0", interval: float = RADIO_CHECK_INTERVAL,
- counter: Path | None = None, name: Callable[[str], str] | None = None):
+ counter: Path | None = None):
self.log, self.interface, self.interval = log, interface, interval
- if name is None:
- from openpilot.starpilot.system.starport.network import wifi_network_name as name
- self.name = name
self.counter = counter or Path(f"/sys/class/net/{interface}/carrier_changes")
self.changed_at: float | None = None
- self.network = ""
self._stop = threading.Event()
self._thread = threading.Thread(target=self._run, name="aa_radio_watch", daemon=True)
@@ -281,11 +276,11 @@ class SharedRadioWatch:
def stop(self) -> None:
self._stop.set()
- def recent(self, now: float | None = None) -> str | None:
- """The other network's name if it changed within RADIO_CHANGE_WINDOW ("" if unnamed), else None."""
+ def recent(self, now: float | None = None) -> bool:
+ """Whether the other network joined or roamed within RADIO_CHANGE_WINDOW."""
changed_at = self.changed_at
now = time.monotonic() if now is None else now
- return self.network if changed_at is not None and now - changed_at <= RADIO_CHANGE_WINDOW else None
+ return changed_at is not None and now - changed_at <= RADIO_CHANGE_WINDOW
def _read(self) -> str | None:
try:
@@ -300,13 +295,8 @@ class SharedRadioWatch:
if current is None or current == last:
continue
last = current
- self.network = "" # named below; D-Bus can be slow, and the change must count from now
self.changed_at = time.monotonic()
- try:
- self.network = self.name(self.interface)
- except Exception:
- self.network = ""
- self.log("other_wifi_changed", interface=self.interface, network=self.network)
+ self.log("other_wifi_changed", interface=self.interface)
class EventLog:
@@ -597,11 +587,11 @@ class Supervisor:
cache, replies = dict(self.config["rfcomm_cache"]), dict(self.config.get("modern_reply_cache", {}))
cached = cache.pop(address, None) is not None
cached = replies.pop(address, None) is not None or cached
- if not (chosen or companion or cached):
+ learned = self._ladder.forget(address)
+ if not (chosen or companion or cached or learned):
return False # nothing of this device was kept
self.config.update(rfcomm_cache=cache, modern_reply_cache=replies)
self._modern_reply.pop(address, None)
- self._ladder.forget(address)
if chosen:
if self._bluez is not None:
self._bluez.release()
@@ -846,7 +836,7 @@ class Supervisor:
return
self._adopt_at = now
try:
- car, candidates = choose_only_car(self._phone().devices())
+ car, candidates = choose_only_car(self.devices())
except Exception as error:
self._adopt_note("car_adopt_unavailable", kind=type(error).__name__, error=str(error)[:120])
return
@@ -860,7 +850,7 @@ class Supervisor:
return
try:
self.select_receiver(car["address"], car.get("name") or "")
- except (RuntimeError, ValueError) as error:
+ except Exception as error:
self._adopt_note("car_adopt_failed", error=str(error))
return
finally:
@@ -1369,7 +1359,7 @@ class Supervisor:
reach = reachability.classify(stage, type(error).__name__, f"{message} {error}")
self.log("attempt_failed", stage=stage, error=message, kind=type(error).__name__, where=_where(error), reach=reach)
if isinstance(error, SharedRadioStall):
- reconnect_notice.write(reconnect_notice.shared_radio_text(error.network))
+ reconnect_notice.write(reconnect_notice.SHARED_RADIO_TEXT)
if isinstance(error, StrayLinkError):
self._clear_stray_link()
car_first = True # the car pages right back once its link drops; let it, rather than crossing it again
@@ -1414,8 +1404,7 @@ class Supervisor:
if isinstance(error, AuthenticationRejected):
return "The car rejected the Android Auto identity; it may have expired"
if isinstance(error, SharedRadioStall):
- joined = f"joined {error.network}" if error.network else "was switching networks"
- return f"The car's Wi-Fi stalled while the comma's own Wi-Fi {joined}; reconnecting"
+ return "The car's Wi-Fi stalled while the comma's own Wi-Fi was switching networks; reconnecting"
text = str(error) or type(error).__name__
if stage == "authenticating" and "certificate" in text.lower():
text += " (set verify_head_unit false in config.json to test without verifying the car)"
diff --git a/openpilot/starpilot/system/starport/tests/test_bootstrap_receivers.py b/openpilot/starpilot/system/starport/tests/test_bootstrap_receivers.py
index 4c41072a91..4622c9bcd2 100644
--- a/openpilot/starpilot/system/starport/tests/test_bootstrap_receivers.py
+++ b/openpilot/starpilot/system/starport/tests/test_bootstrap_receivers.py
@@ -185,16 +185,3 @@ def test_version_four_head_units_get_a_phone_shaped_version_reply():
def test_older_head_units_and_the_first_try_keep_the_reply_that_works_today():
for reply in (version_reply(FORD_V16), version_reply(KIA_V41, modern=False)):
assert reply[4] == [0] and 5 not in reply and 6 not in reply
-
-
-def test_only_a_silent_v4_head_unit_asks_for_the_other_reply():
- timeout = bs.BootstrapTimeout("wifi_start", "head unit did not answer in time")
- boot, _ = scripted([])
- boot.version_major = 4
- assert boot.silent_after_modern_version(timeout)
- boot.car_started = True
- assert not boot.silent_after_modern_version(timeout)
- boot.car_started, boot.version_major = False, 1
- assert not boot.silent_after_modern_version(timeout)
- boot.version_major = 4
- assert not boot.silent_after_modern_version(bs.BootstrapTimeout("wifi_info", "head unit did not answer in time"))
diff --git a/openpilot/starpilot/system/starport/tests/test_car_adoption.py b/openpilot/starpilot/system/starport/tests/test_car_adoption.py
index 1c10590dc3..5c7364f364 100644
--- a/openpilot/starpilot/system/starport/tests/test_car_adoption.py
+++ b/openpilot/starpilot/system/starport/tests/test_car_adoption.py
@@ -109,3 +109,13 @@ def test_an_unreadable_bluetooth_service_is_reported_once(sup):
for tick in range(4):
sup._adopt_only_car(1000.0 + 25 * tick)
assert names(sup).count("car_adopt_unavailable") == 1
+
+
+def test_a_bluetooth_error_while_choosing_the_car_is_reported_not_raised(sup):
+ sup._phone().paired_devices = [(CAR, "Civic", True)]
+
+ def rejected(address, name=""):
+ raise Exception("Bluetooth rejected")
+ sup.select_receiver = rejected
+ sup._adopt_only_car(1000.0)
+ assert "car_adopt_failed" in names(sup) and sup.config["receiver_address"] == ""
diff --git a/openpilot/starpilot/system/starport/tests/test_forget_car.py b/openpilot/starpilot/system/starport/tests/test_forget_car.py
index d20bf89df7..4bb28a128c 100644
--- a/openpilot/starpilot/system/starport/tests/test_forget_car.py
+++ b/openpilot/starpilot/system/starport/tests/test_forget_car.py
@@ -77,3 +77,11 @@ def test_the_version_reply_a_car_started_with_is_kept_and_forgotten_with_it(sup)
assert saved()["modern_reply_cache"] == {ADAPTER: True}
daemon.handle(sup, {"command": "forget_receiver", "address": ADAPTER})
assert saved()["modern_reply_cache"] == {} and ADAPTER not in sup._modern_reply
+
+
+def test_a_car_known_only_to_the_setup_ladder_is_still_forgotten(sup):
+ sup._ladder.strategy(OTHER, False)
+ sup._ladder.failed(OTHER, sup._ladder.strategy(OTHER, False), ladder_failure=True, video_seen=False, version_major=4, adapter=False)
+ assert OTHER.upper() in sup._ladder.cars
+ daemon.handle(sup, {"command": "forget_receiver", "address": OTHER})
+ assert OTHER.upper() not in sup._ladder.cars
diff --git a/openpilot/starpilot/system/starport/tests/test_shared_radio_grace.py b/openpilot/starpilot/system/starport/tests/test_shared_radio_grace.py
index 33c02df4c6..ee3cbce44e 100644
--- a/openpilot/starpilot/system/starport/tests/test_shared_radio_grace.py
+++ b/openpilot/starpilot/system/starport/tests/test_shared_radio_grace.py
@@ -17,42 +17,41 @@ def identity(tmp_path_factory):
return make_identity(tmp_path_factory.mktemp("identity"))
-def stalled(identity, network: str | None):
+def stalled(identity, changed: bool):
hu = FakeHeadUnit(identity)
session = streaming_session(hu, identity)
- session.radio_change = lambda: network
+ session.radio_change = lambda: changed
hu.hold_acks.set()
return hu, session, sent_frame(hu, session)
def test_no_radio_change_keeps_the_standard_limit(identity):
- hu, session, sent = stalled(identity, None)
+ hu, session, sent = stalled(identity, False)
with pytest.raises(TimeoutError, match="2.5 s") as raised:
session.check_progress(now=sent + ProjectionSession.ACK_TIMEOUT + 0.05)
assert not isinstance(raised.value, SharedRadioStall)
end_session(session, hu)
-def test_radio_change_waits_up_to_the_grace_limit_then_names_the_network(identity):
- hu, session, sent = stalled(identity, "Huckleberrys House")
+def test_radio_change_waits_up_to_the_grace_limit(identity):
+ hu, session, sent = stalled(identity, True)
session.check_progress(now=sent + ProjectionSession.ACK_TIMEOUT + 0.05)
session.check_progress(now=sent + ProjectionSession.ACK_GRACE_TIMEOUT - 0.1)
- with pytest.raises(SharedRadioStall) as raised:
+ with pytest.raises(SharedRadioStall):
session.check_progress(now=sent + ProjectionSession.ACK_GRACE_TIMEOUT + 0.05)
- assert raised.value.network == "Huckleberrys House"
end_session(session, hu)
def test_grace_started_by_a_change_survives_the_change_ageing_out(identity):
- hu, session, sent = stalled(identity, "Home")
+ hu, session, sent = stalled(identity, True)
session.check_progress(now=sent + ProjectionSession.ACK_TIMEOUT + 0.05)
- session.radio_change = lambda: None # same stall: the decision was made when it began
+ session.radio_change = lambda: False # same stall: the decision was made when it began
session.check_progress(now=sent + ProjectionSession.ACK_GRACE_TIMEOUT - 0.1)
end_session(session, hu)
def test_acks_resuming_during_the_grace_keep_the_session_and_log_the_gap(identity):
- hu, session, sent = stalled(identity, "Home")
+ hu, session, sent = stalled(identity, True)
events = []
session.event = lambda name, **fields: events.append((name, fields))
session.check_progress(now=sent + ProjectionSession.ACK_TIMEOUT + 0.05)
@@ -66,7 +65,7 @@ def test_acks_resuming_during_the_grace_keep_the_session_and_log_the_gap(identit
def test_car_taking_its_screen_back_during_the_grace_reconnects(identity):
- hu, session, sent = stalled(identity, "Home")
+ hu, session, sent = stalled(identity, True)
session.check_progress(now=sent + ProjectionSession.ACK_TIMEOUT + 0.05)
hu.set_focus(False)
pump_until(session, lambda: not session.focused)
@@ -84,64 +83,35 @@ def test_unfocus_outside_a_grace_is_the_normal_reverse_camera(identity):
end_session(session, hu)
-def home(interface):
- return "Huckleberrys House"
-
-
-def test_watch_reports_a_carrier_change_with_the_network_name(tmp_path, monkeypatch):
+def test_watch_reports_a_carrier_change_without_naming_the_network(tmp_path):
counter = tmp_path / "carrier_changes"
counter.write_text("4\n")
logged = []
- watch = SharedRadioWatch(lambda name, **fields: logged.append((name, fields)), interval=0.01, counter=counter, name=home).start()
+ watch = SharedRadioWatch(lambda name, **fields: logged.append((name, fields)), interval=0.01, counter=counter).start()
try:
time.sleep(0.05)
- assert watch.recent() is None # the starting value is not a change
+ assert not watch.recent() # the starting value is not a change
counter.write_text("5\n")
deadline = time.monotonic() + 2
- while watch.recent() != "Huckleberrys House":
+ while not watch.recent():
assert time.monotonic() < deadline
time.sleep(0.01)
- assert logged == [("other_wifi_changed", {"interface": "wlan0", "network": "Huckleberrys House"})]
+ assert logged == [("other_wifi_changed", {"interface": "wlan0"})]
assert watch.changed_at is not None
- assert watch.recent(now=watch.changed_at + supervisor_module.RADIO_CHANGE_WINDOW + 0.1) is None
+ assert not watch.recent(now=watch.changed_at + supervisor_module.RADIO_CHANGE_WINDOW + 0.1)
finally:
watch.stop()
def test_watch_without_the_counter_never_reports(tmp_path):
- watch = SharedRadioWatch(lambda *a, **k: None, interval=0.01, counter=tmp_path / "missing", name=home).start()
+ watch = SharedRadioWatch(lambda *a, **k: None, interval=0.01, counter=tmp_path / "missing").start()
try:
time.sleep(0.05)
- assert watch.recent() is None
+ assert not watch.recent()
finally:
watch.stop()
-def test_name_lookup_failure_still_counts_the_change(tmp_path):
- def broken(interface):
- raise RuntimeError("NetworkManager timed out")
-
- counter = tmp_path / "carrier_changes"
- counter.write_text("1")
- watch = SharedRadioWatch(lambda *a, **k: None, interval=0.01, counter=counter, name=broken).start()
- try:
- counter.write_text("2")
- deadline = time.monotonic() + 2
- while watch.recent() is None:
- assert time.monotonic() < deadline
- time.sleep(0.01)
- assert watch.recent() == ""
- finally:
- watch.stop()
-
-
-def test_notice_text_is_short_and_plain():
- assert reconnect_notice.shared_radio_text("Huckleberrys House") == "Reconnected: comma Wi-Fi Joined Huckleberrys House"
- assert reconnect_notice.shared_radio_text("") == "Reconnected: comma Wi-Fi Was Switching Networks"
- long = reconnect_notice.shared_radio_text("A" * 60)
- assert long.endswith("...") and len(long) <= len("Reconnected: comma Wi-Fi Joined ") + reconnect_notice.NAME_MAX
-
-
def test_notice_is_shown_once_and_only_while_fresh(tmp_path):
path = tmp_path / "notice.json"
reconnect_notice.write("Reconnected", path, now=1000.0)
@@ -153,15 +123,7 @@ def test_notice_is_shown_once_and_only_while_fresh(tmp_path):
def test_galaxy_status_says_what_happened():
- text = Supervisor._describe_error("streaming", SharedRadioStall("stalled", "Huckleberrys House"))
- assert text == "The car's Wi-Fi stalled while the comma's own Wi-Fi joined Huckleberrys House; reconnecting"
- assert "switching networks" in Supervisor._describe_error("streaming", SharedRadioStall("stalled", ""))
+ text = Supervisor._describe_error("streaming", SharedRadioStall("stalled"))
+ assert text == "The car's Wi-Fi stalled while the comma's own Wi-Fi was switching networks; reconnecting"
assert stream_loss_kind(text) == "shared_radio" # bug reports file it apart from ordinary stalls
assert stream_loss_kind("projecting: Video acknowledgement older than 2.5 s") == "stalled"
-
-
-
-def test_saved_profile_names_become_the_network_name():
- from openpilot.starpilot.system.starport.network import profile_network_name
- assert profile_network_name("openpilot connection Huckleberrys House") == "Huckleberrys House"
- assert profile_network_name("iPhone") == "iPhone"