Skip to content

Commit

Permalink
C3: detect metered Android hotspot (commaai#23734)
Browse files Browse the repository at this point in the history
* C3: detect metered networks

* show in ui

* fix text layout

* bump cereal

* revert ui changes

* set networkMetered

* add athena method

* add metered logging to uploader

* use in athena uploader

* remove param

* use networkmanager properties to set cell to unmetered

* fix indentation

* no need to check

* bump cereal

* review

* bump cereal
# Conflicts:
#	cereal
  • Loading branch information
pd0wm authored and budney committed Mar 27, 2022
1 parent 77dba36 commit a6b5433
Show file tree
Hide file tree
Showing 7 changed files with 66 additions and 25 deletions.
2 changes: 1 addition & 1 deletion cereal
Submodule cereal updated 1 files
+1 −0 log.capnp
33 changes: 18 additions & 15 deletions selfdrive/athena/athenad.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,6 @@ def upload_handler(end_event: threading.Event) -> None:
sm = messaging.SubMaster(['deviceState'])
tid = threading.get_ident()

cellular_unmetered = Params().get_bool("CellularUnmetered")

while not end_event.is_set():
cur_upload_items[tid] = None

Expand All @@ -181,46 +179,45 @@ def upload_handler(end_event: threading.Event) -> None:
cloudlog.event("athena.upload_handler.expired", item=cur_upload_items[tid], error=True)
continue

# Check if uploading over cell is allowed
# Check if uploading over metered connection is allowed
sm.update(0)
cell = sm['deviceState'].networkType not in [NetworkType.wifi, NetworkType.ethernet]
if cell and (not cur_upload_items[tid].allow_cellular) and (not cellular_unmetered):
metered = sm['deviceState'].networkMetered
network_type = sm['deviceState'].networkType.raw
if metered and (not cur_upload_items[tid].allow_cellular):
retry_upload(tid, end_event, False)
continue

try:
def cb(sz, cur):
# Abort transfer if connection changed to cell after starting upload
# Abort transfer if connection changed to metered after starting upload
sm.update(0)
cell = sm['deviceState'].networkType not in [NetworkType.wifi, NetworkType.ethernet]
if cell and (not cur_upload_items[tid].allow_cellular) and (not cellular_unmetered):
metered = sm['deviceState'].networkMetered
if metered and (not cur_upload_items[tid].allow_cellular):
raise AbortTransferException

cur_upload_items[tid] = cur_upload_items[tid]._replace(progress=cur / sz if sz else 1)


network_type = sm['deviceState'].networkType.raw
fn = cur_upload_items[tid].path
try:
sz = os.path.getsize(fn)
except OSError:
sz = -1

cloudlog.event("athena.upload_handler.upload_start", fn=fn, sz=sz, network_type=network_type)
cloudlog.event("athena.upload_handler.upload_start", fn=fn, sz=sz, network_type=network_type, metered=metered)
response = _do_upload(cur_upload_items[tid], cb)

if response.status_code not in (200, 201, 403, 412):
cloudlog.event("athena.upload_handler.retry", status_code=response.status_code, fn=fn, sz=sz, network_type=network_type)
cloudlog.event("athena.upload_handler.retry", status_code=response.status_code, fn=fn, sz=sz, network_type=network_type, metered=metered)
retry_upload(tid, end_event)
else:
cloudlog.event("athena.upload_handler.success", fn=fn, sz=sz, network_type=network_type)
cloudlog.event("athena.upload_handler.success", fn=fn, sz=sz, network_type=network_type, metered=metered)

UploadQueueCache.cache(upload_queue)
except (requests.exceptions.Timeout, requests.exceptions.ConnectionError, requests.exceptions.SSLError):
cloudlog.event("athena.upload_handler.timeout", fn=fn, sz=sz, network_type=network_type)
cloudlog.event("athena.upload_handler.timeout", fn=fn, sz=sz, network_type=network_type, metered=metered)
retry_upload(tid, end_event)
except AbortTransferException:
cloudlog.event("athena.upload_handler.abort", fn=fn, sz=sz, network_type=network_type)
cloudlog.event("athena.upload_handler.abort", fn=fn, sz=sz, network_type=network_type, metered=metered)
retry_upload(tid, end_event, False)

except queue.Empty:
Expand Down Expand Up @@ -459,6 +456,12 @@ def getNetworkType():
return HARDWARE.get_network_type()


@dispatcher.add_method
def getNetworkMetered():
network_type = HARDWARE.get_network_type()
return HARDWARE.get_network_metered(network_type)


@dispatcher.add_method
def getNetworks():
return HARDWARE.get_networks()
Expand Down
1 change: 0 additions & 1 deletion selfdrive/common/params.cc
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,6 @@ std::unordered_map<std::string, uint32_t> keys = {
{"CarParams", CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON},
{"CarParamsCache", CLEAR_ON_MANAGER_START},
{"CarVin", CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON},
{"CellularUnmetered", PERSISTENT},
{"CompletedTrainingVersion", PERSISTENT},
{"ControlsReady", CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON},
{"CurrentRoute", CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON},
Expand Down
7 changes: 7 additions & 0 deletions selfdrive/hardware/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@
from collections import namedtuple
from typing import Dict

from cereal import log

ThermalConfig = namedtuple('ThermalConfig', ['cpu', 'gpu', 'mem', 'bat', 'ambient', 'pmic'])
NetworkType = log.DeviceState.NetworkType


class HardwareBase(ABC):
@staticmethod
Expand Down Expand Up @@ -67,6 +71,9 @@ def get_sim_info(self):
def get_network_strength(self, network_type):
pass

def get_network_metered(self, network_type) -> bool:
return network_type not in (NetworkType.none, NetworkType.wifi, NetworkType.ethernet)

@staticmethod
def set_bandwidth_limit(upload_speed_kbps: int, download_speed_kbps: int) -> None:
pass
Expand Down
32 changes: 30 additions & 2 deletions selfdrive/hardware/tici/hardware.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

NM = 'org.freedesktop.NetworkManager'
NM_CON_ACT = NM + '.Connection.Active'
NM_DEV = NM + '.Device'
NM_DEV_WL = NM + '.Device.Wireless'
NM_AP = NM + '.AccessPoint'
DBUS_PROPS = 'org.freedesktop.DBus.Properties'
Expand All @@ -37,6 +38,13 @@ class MM_MODEM_STATE(IntEnum):
CONNECTING = 10
CONNECTED = 11

class NMMetered(IntEnum):
NM_METERED_UNKNOWN = 0
NM_METERED_YES = 1
NM_METERED_NO = 2
NM_METERED_GUESS_YES = 3
NM_METERED_GUESS_NO = 4

TIMEOUT = 0.1

NetworkType = log.DeviceState.NetworkType
Expand Down Expand Up @@ -91,11 +99,10 @@ def get_network_type(self):
primary_connection = self.nm.Get(NM, 'PrimaryConnection', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)
primary_connection = self.bus.get_object(NM, primary_connection)
primary_type = primary_connection.Get(NM_CON_ACT, 'Type', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)
primary_id = primary_connection.Get(NM_CON_ACT, 'Id', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)

if primary_type == '802-3-ethernet':
return NetworkType.ethernet
elif primary_type == '802-11-wireless' and primary_id != 'Hotspot':
elif primary_type == '802-11-wireless':
return NetworkType.wifi
else:
active_connections = self.nm.Get(NM, 'ActiveConnections', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)
Expand Down Expand Up @@ -218,6 +225,27 @@ def get_network_strength(self, network_type):

return network_strength

def get_network_metered(self, network_type) -> bool:
try:
primary_connection = self.nm.Get(NM, 'PrimaryConnection', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)
primary_connection = self.bus.get_object(NM, primary_connection)
primary_devices = primary_connection.Get(NM_CON_ACT, 'Devices', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)

for dev in primary_devices:
dev_obj = self.bus.get_object(NM, str(dev))
metered_prop = dev_obj.Get(NM_DEV, 'Metered', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)

if network_type == NetworkType.wifi:
if metered_prop in [NMMetered.NM_METERED_YES, NMMetered.NM_METERED_GUESS_YES]:
return True
elif network_type in [NetworkType.cell2G, NetworkType.cell3G, NetworkType.cell4G, NetworkType.cell5G]:
if metered_prop == NMMetered.NM_METERED_NO:
return False
except Exception:
pass

return super().get_network_metered(network_type)

@staticmethod
def set_bandwidth_limit(upload_speed_kbps: int, download_speed_kbps: int) -> None:
upload_speed_kbps = int(upload_speed_kbps) # Ensure integer value
Expand Down
11 changes: 6 additions & 5 deletions selfdrive/loggerd/uploader.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,14 +165,14 @@ def normal_upload(self, key, fn):

return self.last_resp

def upload(self, key, fn, network_type):
def upload(self, key, fn, network_type, metered):
try:
sz = os.path.getsize(fn)
except OSError:
cloudlog.exception("upload: getsize failed")
return False

cloudlog.event("upload_start", key=key, fn=fn, sz=sz, network_type=network_type)
cloudlog.event("upload_start", key=key, fn=fn, sz=sz, network_type=network_type, metered=metered)

if sz == 0:
try:
Expand All @@ -195,10 +195,10 @@ def upload(self, key, fn, network_type):
self.last_time = time.monotonic() - start_time
self.last_speed = (sz / 1e6) / self.last_time
success = True
cloudlog.event("upload_success" if stat.status_code != 412 else "upload_ignored", key=key, fn=fn, sz=sz, network_type=network_type)
cloudlog.event("upload_success" if stat.status_code != 412 else "upload_ignored", key=key, fn=fn, sz=sz, network_type=network_type, metered=metered)
else:
success = False
cloudlog.event("upload_failed", stat=stat, exc=self.last_exc, key=key, fn=fn, sz=sz, network_type=network_type)
cloudlog.event("upload_failed", stat=stat, exc=self.last_exc, key=key, fn=fn, sz=sz, network_type=network_type, metered=metered)

return success

Expand Down Expand Up @@ -247,7 +247,7 @@ def uploader_fn(exit_event):

key, fn = d

success = uploader.upload(key, fn, sm['deviceState'].networkType.raw)
success = uploader.upload(key, fn, sm['deviceState'].networkType.raw, sm['deviceState'].networkMetered)
if success:
backoff = 0.1
elif allow_sleep:
Expand All @@ -257,6 +257,7 @@ def uploader_fn(exit_event):

pm.send("uploaderState", uploader.get_msg())


def main():
uploader_fn(threading.Event())

Expand Down
5 changes: 4 additions & 1 deletion selfdrive/thermald/thermald.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
PANDA_STATES_TIMEOUT = int(1000 * 2.5 * DT_TRML) # 2.5x the expected pandaState frequency

ThermalBand = namedtuple("ThermalBand", ['min_temp', 'max_temp'])
HardwareState = namedtuple("HardwareState", ['network_type', 'network_strength', 'network_info', 'nvme_temps', 'modem_temps'])
HardwareState = namedtuple("HardwareState", ['network_type', 'network_metered', 'network_strength', 'network_info', 'nvme_temps', 'modem_temps'])

# List of thermal bands. We will stay within this region as long as we are within the bounds.
# When exiting the bounds, we'll jump to the lower or higher band. Bands are ordered in the dict.
Expand Down Expand Up @@ -95,6 +95,7 @@ def hw_state_thread(end_event, hw_queue):

hw_state = HardwareState(
network_type=network_type,
network_metered=HARDWARE.get_network_metered(network_type),
network_strength=HARDWARE.get_network_strength(network_type),
network_info=HARDWARE.get_network_info(),
nvme_temps=HARDWARE.get_nvme_temperatures(),
Expand Down Expand Up @@ -144,6 +145,7 @@ def thermald_thread(end_event, hw_queue):

last_hw_state = HardwareState(
network_type=NetworkType.none,
network_metered=False,
network_strength=NetworkStrength.unknown,
network_info=None,
nvme_temps=[],
Expand Down Expand Up @@ -205,6 +207,7 @@ def thermald_thread(end_event, hw_queue):
msg.deviceState.gpuUsagePercent = int(round(HARDWARE.get_gpu_usage_percent()))

msg.deviceState.networkType = last_hw_state.network_type
msg.deviceState.networkMetered = last_hw_state.network_metered
msg.deviceState.networkStrength = last_hw_state.network_strength
if last_hw_state.network_info is not None:
msg.deviceState.networkInfo = last_hw_state.network_info
Expand Down

0 comments on commit a6b5433

Please sign in to comment.