Skip to content

Commit

Permalink
macOS: work with macOS >= 14.4 again.
Browse files Browse the repository at this point in the history
rename get_signal() => scan_signal()
rename parse_signal() => get_signal()
  • Loading branch information
scivision committed Sep 19, 2024
1 parent 06d79cf commit ec89c4d
Show file tree
Hide file tree
Showing 11 changed files with 139 additions and 110 deletions.
59 changes: 0 additions & 59 deletions macos_corelocation.py

This file was deleted.

6 changes: 3 additions & 3 deletions src/mozloc/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from .base import log_wifi_loc
from .modules import get_signal, parse_signal, cli_config_check
from .modules import get_signal, scan_signal, config_check

__version__ = "1.7.0"
__version__ = "1.8.0"

__all__ = ["log_wifi_loc", "get_signal", "parse_signal", "cli_config_check"]
__all__ = ["log_wifi_loc", "get_signal", "scan_signal", "config_check"]
4 changes: 2 additions & 2 deletions src/mozloc/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from pprint import pprint

from .base import log_wifi_loc, process_file
from .modules import parse_signal, get_signal
from .modules import scan_signal, get_signal


p = argparse.ArgumentParser()
Expand All @@ -35,7 +35,7 @@
args = p.parse_args()

if args.dump:
pprint(parse_signal(get_signal()))
pprint(get_signal(scan_signal()))
elif args.infile:
process_file(args.infile, mozilla_url=args.url)
else:
Expand Down
11 changes: 6 additions & 5 deletions src/mozloc/airport.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
""" MacOS airport functions
""" macOS airport functions
Airport was removed from macOS 14.4+, so these functions no longer are relevant.
Airport was removed from macOS 14.4+.
Newer macOS use macos_corelocation.py instead.
"""

import logging
Expand All @@ -12,7 +13,7 @@
from .exe import get_airport, running_as_root


def cli_config_check() -> bool:
def config_check() -> bool:
# %% check that Airport is available and WiFi is active

try:
Expand All @@ -32,7 +33,7 @@ def cli_config_check() -> bool:
return False


def get_signal() -> str:
def scan_signal() -> str:
try:
ret = subprocess.check_output([get_airport(), "--scan"], text=True, timeout=30)
except subprocess.CalledProcessError as err:
Expand All @@ -41,7 +42,7 @@ def get_signal() -> str:
return ret


def parse_signal(raw: str) -> pandas.DataFrame:
def get_signal(raw: str):
isroot = running_as_root()

psudo = r"\s*([0-9a-zA-Z\s\-\.]+)\s+([0-9a-f]{2}(?::[0-9a-f]{2}){5})\s+(-\d{2,3})"
Expand Down
11 changes: 6 additions & 5 deletions src/mozloc/base.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from __future__ import annotations
from time import sleep
from pathlib import Path
import logging
from pprint import pprint

from .modules import get_signal, parse_signal, cli_config_check
from .modules import get_signal, scan_signal, config_check
from .web import get_loc_mozilla

HEADER = "time lat lon accuracy NumBSSIDs"
Expand All @@ -15,7 +16,7 @@ def process_file(file: Path, mozilla_url: str):
"""

raw = Path(file).expanduser().read_text()
dat = parse_signal(raw)
dat = get_signal(raw)
pprint(dat)
loc = get_loc_mozilla(dat, url=mozilla_url)

Expand All @@ -33,13 +34,13 @@ def log_wifi_loc(cadence_sec: float, mozilla_url: str, logfile: Path | None = No
print(f"updating every {cadence_sec} seconds")
print(HEADER)

if not cli_config_check():
if not config_check():
raise ConnectionError("Could not connect to WiFi hardware")
# nmcli errored for less than about 0.2 sec.
sleep(0.5)
while True:
raw = get_signal()
dat = parse_signal(raw)
raw = scan_signal()
dat = get_signal(raw)
if len(dat) < 2:
logging.warning(f"cannot locate since at least 2 BSSIDs required\n{dat}")
sleep(cadence_sec)
Expand Down
86 changes: 86 additions & 0 deletions src/mozloc/macos_corelocation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# /usr/bin/env python3
"""
MUST USE SYSTEM PYTHON /usr/bin/python3
DON'T USE sudo as LocationServices Python won't pop up.
TODO: even with all this, BSSID is still (null) / None
LocationServices Python app becomes available to enable in
Settings > Privacy > Location Services
pip install pyobjc
from https://forums.developer.apple.com/forums/thread/748161?answerId=782574022#782574022
Ref: https://docs.python.org/3/using/mac.html#gui-programming
"""

import CoreLocation
import time
import logging
import pandas

import objc


def config_check() -> bool:
"""
Need authorization to get BSSID
"""

mgr = CoreLocation.CLLocationManager.alloc().init()
# mgr = CoreLocation.CLLocationManager.new()
# mgr.requestAlwaysAuthorization()
mgr.startUpdatingLocation()

max_wait = 10
# Get the current authorization status for Python
# https://stackoverflow.com/a/75843844
for i in range(1, max_wait):
s = mgr.authorizationStatus()
if s in {3, 4}:
print("Python has been authorized for location services")
return True
if i == max_wait - 1:
logging.error("Unable to obtain authorization")
return False
print(f"Waiting for authorization... do you see the Location Services popup window? {s}")
time.sleep(0.5)

return False


def get_signal(networks):
# Get the current location

dat: list[dict[str, str]] = []

for network in networks:
# print(f"{network.ssid()} {network.bssid()} {network.rssi()} channel {network.channel()}")
d = {"ssid": network.ssid(), "signalStrength": network.rssi()}
if network.bssid() is not None:
d["macAddress"] = network.bssid()
dat.append(d)

return pandas.DataFrame(dat)


def scan_signal():

bundle_path = "/System/Library/Frameworks/CoreWLAN.framework"

objc.loadBundle("CoreWLAN", bundle_path=bundle_path, module_globals=globals())

# https://developer.apple.com/documentation/corewlan/cwinterface
# iface = CWInterface.interface() # not recommended, low-level
# https://developer.apple.com/documentation/corewlan/cwwificlient
iface = CoreLocation.CWWiFiClient.sharedWiFiClient().interface()

logging.info(f"WiFi interface {iface.interfaceName()}")

# need to run once to warmup -- otherwise all SSID are "(null)"
iface.scanForNetworksWithName_includeHidden_error_(None, True, None)

networks, error = iface.scanForNetworksWithName_includeHidden_error_(None, True, None)

return networks
23 changes: 13 additions & 10 deletions src/mozloc/modules.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import sys
import platform


match sys.platform:
case "win32":
from .netsh import cli_config_check, get_signal, parse_signal
case "linux":
from .netman import cli_config_check, get_signal, parse_signal
case "darwin":
from .airport import cli_config_check, get_signal, parse_signal
case _:
raise ImportError(f"MozLoc doesn't work with platform {sys.platform}")
if sys.platform == "win32":
from .netsh import config_check, get_signal, scan_signal
elif sys.platform == "linux":
from .netman import config_check, get_signal, scan_signal
elif sys.platform == "darwin":
if tuple(map(int, platform.mac_ver()[0].split("."))) < (14, 4):
from .airport import config_check, get_signal, scan_signal
else:
from .macos_corelocation import config_check, get_signal, scan_signal
else:
raise ImportError(f"MozLoc doesn't work with platform {sys.platform}")


__all__ = ["cli_config_check", "get_signal", "parse_signal"]
__all__ = ["config_check", "get_signal", "scan_signal"]
6 changes: 3 additions & 3 deletions src/mozloc/netman.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from .exe import get_exe


def cli_config_check() -> bool:
def config_check() -> bool:
# %% check that NetworkManager CLI is available and WiFi is active
exe = get_exe("nmcli")

Expand All @@ -35,7 +35,7 @@ def cli_config_check() -> bool:
return False


def get_signal() -> str:
def scan_signal() -> str:
exe = get_exe("nmcli")

cmd = [exe, "-g", "SSID,BSSID,FREQ,SIGNAL", "device", "wifi"]
Expand All @@ -61,7 +61,7 @@ def get_signal() -> str:
return ret


def parse_signal(raw: str) -> pandas.DataFrame:
def get_signal(raw: str):
dat = pandas.read_csv(
io.StringIO(raw),
sep=r"(?<!\\):",
Expand Down
6 changes: 3 additions & 3 deletions src/mozloc/netsh.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from .exe import get_exe


def cli_config_check() -> bool:
def config_check() -> bool:
# %% check that NetSH EXE is available and WiFi is active
exe = get_exe("netsh")

Expand All @@ -35,7 +35,7 @@ def cli_config_check() -> bool:
return False


def get_signal() -> str:
def scan_signal() -> str:
"""
get signal strength using EXE
Expand All @@ -52,7 +52,7 @@ def get_signal() -> str:
return ret


def parse_signal(raw: str) -> pandas.DataFrame:
def get_signal(raw: str):
dat: list[dict[str, str]] = []
out = io.StringIO(raw)

Expand Down
17 changes: 17 additions & 0 deletions src/mozloc/tests/test_mozloc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import pytest
import os
import pandas
import mozloc

is_ci = os.environ.get("CI", "").lower() == "true"


@pytest.mark.skipif(is_ci, reason="CI doesn't usually have WiFi")
def test_signal():
if not mozloc.config_check():
pytest.skip("WiFi not available")

loc = mozloc.get_signal(mozloc.scan_signal())

assert isinstance(loc, pandas.DataFrame)
assert -130 < int(loc["signalStrength"][0]) < 0, "impossible RSSI"
20 changes: 0 additions & 20 deletions src/mozloc/tests/test_netman.py

This file was deleted.

0 comments on commit ec89c4d

Please sign in to comment.